Skip to content

Commit de5df74

Browse files
authored
Merge pull request #228 from link-assistant/issue-227-aec5b6edc4b3
feat: centralize config with lino-arguments, remove Flag module (#227)
2 parents ff0984e + 5613cdc commit de5df74

87 files changed

Lines changed: 3201 additions & 1982 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

EXAMPLES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ echo '{"message":"search web","tools":[{"name":"websearch","params":{"query":"Ty
208208
echo '{"message":"search web","tools":[{"name":"websearch","params":{"query":"React hooks best practices"}}]}' | agent
209209
```
210210

211-
**opencode (requires OPENCODE_EXPERIMENTAL_EXA=true):**
211+
**opencode (requires LINK_ASSISTANT_AGENT_EXPERIMENTAL_EXA=true):**
212212

213213
```bash
214214
echo '{"message":"search web","tools":[{"name":"websearch","params":{"query":"TypeScript latest features"}}]}' | opencode run --format json --model opencode/big-pickle
@@ -226,7 +226,7 @@ echo '{"message":"search code","tools":[{"name":"codesearch","params":{"query":"
226226
echo '{"message":"search code","tools":[{"name":"codesearch","params":{"query":"async/await patterns"}}]}' | agent
227227
```
228228

229-
**opencode (requires OPENCODE_EXPERIMENTAL_EXA=true):**
229+
**opencode (requires LINK_ASSISTANT_AGENT_EXPERIMENTAL_EXA=true):**
230230

231231
```bash
232232
echo '{"message":"search code","tools":[{"name":"codesearch","params":{"query":"React hooks implementation"}}]}' | opencode run --format json --model opencode/big-pickle

TOOLS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ Each tool test verifies:
134134
### No Configuration Required
135135

136136
- All tools work without environment variables or configuration files
137-
- WebSearch and CodeSearch work without `OPENCODE_EXPERIMENTAL_EXA`
137+
- WebSearch and CodeSearch work without `LINK_ASSISTANT_AGENT_EXPERIMENTAL_EXA`
138138
- Batch tool is always enabled, no experimental flag needed
139139

140140
### OpenCode Compatible
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# Case Study: No HTTP Request/Response Logs in `--verbose` Mode (#227)
2+
3+
## Issue
4+
5+
When the agent CLI is used directly with `--verbose`, HTTP request/response logs appear correctly. However, when the agent is spawned by the `solve` command via the `command-stream` library, HTTP verbose logs are silently absent despite `--verbose` being passed.
6+
7+
**Issue:** https://github.com/link-assistant/agent/issues/227
8+
9+
## Evidence
10+
11+
### Working Case (Direct Invocation)
12+
13+
- **Log:** [Gist](https://gist.githubusercontent.com/konard/6a7107ae7987ef5ed19653d4b3b707cb/raw/2763388eac850465dc1a5ec1bb31f5001e9528f0/agent-cli-log.txt)
14+
- **Command:** `echo "hi" | agent --verbose`
15+
- **Version:** 0.18.3
16+
- **Key indicators:**
17+
- `"verboseAtCreation": true` — Flag was true when SDK was created
18+
- 35 debug-level log entries
19+
- HTTP request/response logs present (e.g., `"message": "HTTP request"`)
20+
- `"message": "verbose HTTP logging active"` confirmation
21+
22+
### Broken Case (Subprocess via solve/command-stream)
23+
24+
- **Log:** [Gist](https://gist.githubusercontent.com/konard/79a96bcdf4b1e91ba83ba7bced26976c/raw/6a7f3b01e5a94d5a4bd00e94d1e50aae13c65c0e/solution-draft-log-pr-1775044356765.txt)
25+
- **Command:** `cat prompt.txt | agent --model opencode/minimax-m2.5-free --verbose`
26+
- **Version:** 0.18.1 (same code, different runtime environment)
27+
- **Key indicators:**
28+
- `"verboseAtCreation": false` — Flag was false when SDK was created
29+
- 0 debug-level log entries
30+
- No HTTP request/response logs at all
31+
- `"globalVerboseFetchInstalled": true` — Middleware DID run
32+
- No `"message": "verbose HTTP logging active"` confirmation
33+
34+
## Timeline / Sequence of Events
35+
36+
1. **Solve command** spawns agent CLI as child process via command-stream
37+
2. **Agent CLI starts** — yargs parses arguments, `--verbose` is recognized
38+
3. **Middleware runs**`Flag.setVerbose(true)` is called, `globalThis.fetch` is monkey-patched
39+
4. **BUT**`Flag.VERBOSE` is still `false` at the time HTTP calls are made
40+
5. **Verbose-fetch wrapper** checks `Flag.VERBOSE` → finds it `false` → skips logging
41+
6. **All HTTP logs are silently discarded**
42+
43+
## Root Cause Analysis
44+
45+
The `Flag.VERBOSE` flag is stored as an in-memory `export let` variable in the `flag.ts` module. When `setVerbose(true)` is called in the yargs middleware, it sets this variable to `true`. However, there's evidence that this value can be lost or not propagated correctly in certain runtime environments:
46+
47+
1. **Module re-evaluation:** Bun may re-evaluate modules in some circumstances, resetting the flag to its initial value (`false` from env var check)
48+
2. **Runtime timing:** The flag may be checked before the middleware has fully completed in some environments
49+
3. **No persistence mechanism:** The flag only exists in memory — if the module is reloaded, the flag reverts to its default
50+
51+
The evidence strongly supports this diagnosis:
52+
53+
- `globalVerboseFetchInstalled: true` proves the middleware ran
54+
- `verboseAtCreation: false` proves the flag was `false` at SDK creation time
55+
- 0 debug-level logs proves the flag was `false` during `Log.init()`
56+
- These three facts together indicate the flag was set to `true` in the middleware but was `false` when checked later
57+
58+
## Solution
59+
60+
### 1. Remove Legacy OPENCODE\_\* Environment Variables
61+
62+
All `OPENCODE_*` environment variable support has been removed from the codebase. The codebase now uses exclusively `LINK_ASSISTANT_AGENT_*` prefixed environment variables. This is a clean break from the legacy naming.
63+
64+
### 2. Clean Flag Names
65+
66+
All `Flag.OPENCODE_*` export names have been renamed to clean names without the `OPENCODE_` prefix (e.g., `Flag.VERBOSE`, `Flag.DRY_RUN`, `Flag.CONFIG`), since they are already namespaced under `Flag`.
67+
68+
### 3. Environment Variable Propagation (Verbose Fix)
69+
70+
When `Flag.setVerbose(true)` is called, the environment variable `LINK_ASSISTANT_AGENT_VERBOSE=true` is now also set. This provides:
71+
72+
- **Persistence across module re-evaluations** — env vars survive module reloads
73+
- **Child process inheritance** — subprocesses automatically inherit the flag
74+
- **Redundancy** — two independent sources of truth
75+
76+
### 4. `Flag.isVerbose()` Method with Fallback (Resilience Fix)
77+
78+
A `Flag.isVerbose()` method checks both:
79+
80+
- The in-memory `VERBOSE` flag (fast path)
81+
- The environment variable `LINK_ASSISTANT_AGENT_VERBOSE` (fallback)
82+
83+
All verbose checks across the codebase use `Flag.isVerbose()` instead of directly reading `Flag.VERBOSE`.
84+
85+
### Files Changed
86+
87+
| File | Change |
88+
| ------------------------------ | ------------------------------------------------------------------------------------------- |
89+
| `js/src/config/config.ts`| Centralized config with makeConfig(), removed Flag module, env var propagation |
90+
| `js/src/util/verbose-fetch.ts` | Use `Flag.isVerbose()` |
91+
| `js/src/provider/provider.ts` | Use `Flag.DRY_RUN`, `Flag.ENABLE_EXPERIMENTAL_MODELS` |
92+
| `js/src/config/config.ts` | Use `Flag.CONFIG`, `Flag.CONFIG_DIR`, `Flag.CONFIG_CONTENT` |
93+
| `js/src/bun/index.ts` | Use `Flag.DRY_RUN` |
94+
| `js/src/file/watcher.ts` | Use `Flag.EXPERIMENTAL_WATCHER` |
95+
| `js/src/session/compaction.ts` | Use `Flag.DISABLE_AUTOCOMPACT`, `Flag.DISABLE_PRUNE` |
96+
| `js/src/util/log.ts` | Use `Flag.isVerbose()` |
97+
| `js/src/util/log-lazy.ts` | Use `Flag.isVerbose()` |
98+
| `js/src/index.js` | Use `Flag.DRY_RUN`, `Flag.isVerbose()` |
99+
| `js/src/session/*.ts` | Use `Flag.isVerbose()` |
100+
| `js/tests/` | Updated all tests to use new flag names and env var names |
101+
102+
## Testing
103+
104+
### New Tests (`verbose-env-fallback.test.js`)
105+
106+
1. **Baseline:** `--verbose` flag produces HTTP logs
107+
2. **Env var:** `LINK_ASSISTANT_AGENT_VERBOSE=true` enables HTTP logs without `--verbose` flag
108+
3. **Negative:** No HTTP logs without `--verbose` or env var
109+
4. **Propagation:** `verboseAtCreation: true` confirmed in subprocess
110+
111+
### Existing Tests
112+
113+
- `verbose-hi.test.js` — continues to pass (no regression)
114+
- All dry-run, provider, and verbose logging tests updated and passing
115+
116+
## Evidence from Issue #229
117+
118+
Issue [#229](https://github.com/link-assistant/agent/issues/229) independently confirmed the same root cause with additional detail:
119+
120+
### Key Findings from #229
121+
122+
- **Working:** `OPENCODE_VERBOSE=true echo "hi" | agent --model opencode/minimax-m2.5-free``verboseAtCreation: true`, 18+ HTTP logs
123+
- **Broken:** `echo "hi" | agent --model opencode/minimax-m2.5-free --verbose` (via command-stream) → `verboseAtCreation: false`, 0 HTTP logs
124+
- **Critical observation:** `globalVerboseFetchInstalled: true` but `verboseAtCreation: false` — the interceptor was installed but the flag was not true when checked
125+
126+
### #229 Workaround (confirms root cause)
127+
128+
Setting both env vars AND CLI flag works:
129+
130+
```bash
131+
OPENCODE_VERBOSE=true LINK_ASSISTANT_AGENT_VERBOSE=true echo "hi" | agent --verbose
132+
```
133+
134+
This workaround was adopted in hive-mind's solve command ([link-assistant/hive-mind#1521](https://github.com/link-assistant/hive-mind/issues/1521)).
135+
136+
The fact that setting the env var fixes the problem while `--verbose` alone doesn't (in subprocess context) confirms that the in-memory flag set by yargs middleware is being lost, while the env var persists. This is the exact behavior our fix addresses: `setVerbose(true)` now also sets `LINK_ASSISTANT_AGENT_VERBOSE=true` in `process.env`, and `isVerbose()` checks the env var as fallback.
137+
138+
### Why yargs options alone are insufficient
139+
140+
The `--verbose` flag is processed by yargs middleware in `index.js`, which calls `Flag.setVerbose(true)`. However:
141+
142+
1. `Flag.VERBOSE` is an `export let` variable — a single in-memory binding
143+
2. Many modules (`verbose-fetch.ts`, `log.ts`, `provider.ts`, etc.) import and check this flag independently at call time
144+
3. In Bun's runtime, when modules are re-evaluated (e.g., during subprocess execution), the `export let` binding resets to its initial value
145+
4. The initial value comes from `truthyEnv('LINK_ASSISTANT_AGENT_VERBOSE')` — if the env var isn't set, it defaults to `false`
146+
5. Yargs options are only available in the CLI entry point (`index.js`), not in the deeper modules that check the flag
147+
148+
The env var propagation ensures the verbose state is available globally via `process.env`, which survives module re-evaluation and is accessible from any module without needing to thread yargs options through the entire call chain.
149+
150+
## Env Var Consistency
151+
152+
As part of this fix, all project-owned environment variables have been standardized to use the `LINK_ASSISTANT_AGENT_` prefix exclusively:
153+
154+
| Old Name | New Name |
155+
| ------------------------------- | ---------------------------------------------------- |
156+
| `OPENCODE_VERBOSE` | `LINK_ASSISTANT_AGENT_VERBOSE` |
157+
| `OPENCODE_DRY_RUN` | `LINK_ASSISTANT_AGENT_DRY_RUN` |
158+
| `OPENCODE_CONFIG` | `LINK_ASSISTANT_AGENT_CONFIG` |
159+
| `VERIFY_IMAGES_AT_READ_TOOL` | `LINK_ASSISTANT_AGENT_VERIFY_IMAGES_AT_READ_TOOL` |
160+
| `MCP_DEFAULT_TOOL_CALL_TIMEOUT` | `LINK_ASSISTANT_AGENT_MCP_DEFAULT_TOOL_CALL_TIMEOUT` |
161+
| `MCP_MAX_TOOL_CALL_TIMEOUT` | `LINK_ASSISTANT_AGENT_MCP_MAX_TOOL_CALL_TIMEOUT` |
162+
| `AGENT_CLI_COMPACT` | `LINK_ASSISTANT_AGENT_COMPACT_JSON` |
163+
| `AGENT_STREAM_CHUNK_TIMEOUT_MS` | `LINK_ASSISTANT_AGENT_STREAM_CHUNK_TIMEOUT_MS` |
164+
| `AGENT_STREAM_STEP_TIMEOUT_MS` | `LINK_ASSISTANT_AGENT_STREAM_STEP_TIMEOUT_MS` |
165+
166+
**Note:** Third-party env vars (`CLAUDE_CODE_OAUTH_TOKEN`, `AWS_*`, `GOOGLE_CLOUD_*`, `GEMINI_API_KEY`, etc.) are kept as-is since they are external interfaces defined by other tools/platforms.
167+
168+
## Architectural Improvement: Centralized Config with lino-arguments
169+
170+
### Problem
171+
172+
Before this fix, environment variables were read in multiple scattered locations:
173+
174+
- `flag.ts` — 15+ direct `process.env` reads with manual `truthyEnv()` helpers
175+
- `mcp/index.ts` — 2 direct `process.env` reads for MCP timeouts
176+
- `tool/read.ts` — 1 direct `process.env` read for image verification
177+
- `index.js` — yargs middleware manually syncing CLI args to Flag module
178+
179+
This fragmentation made it hard to:
180+
181+
1. Know what configuration was resolved at startup
182+
2. Debug configuration issues (no central log)
183+
3. Support `.lenv` files or case-insensitive env vars
184+
185+
### Solution: lino-arguments
186+
187+
Adopted [lino-arguments](https://github.com/link-foundation/lino-arguments) to centralize env var resolution:
188+
189+
1. **`config.ts`** — Single source of truth for all configuration. Uses `getenv()` from lino-arguments which provides case-insensitive lookups, type-preserving defaults, and `.lenv` file support.
190+
2. **`flag.ts`** — Thin wrapper that reads from AgentConfig when initialized, falls back to env vars for backward compatibility.
191+
3. **`index.js` middleware** — Calls `initAgentConfig(argv)` once after yargs parsing, merging CLI args and env vars in one place.
192+
4. **Configuration logging** — Always logs the full resolved config as JSON at `info` level, critical for debugging.
193+
194+
### Configuration priority (highest to lowest)
195+
196+
1. CLI arguments (`--verbose`, `--dry-run`, etc.)
197+
2. Environment variables (`LINK_ASSISTANT_AGENT_VERBOSE=true`)
198+
3. `.lenv` file values (via lino-arguments)
199+
4. Code defaults
200+
201+
### Key files
202+
203+
| File | Role |
204+
| ----------------------------- | ---------------------------------------------------- |
205+
| `js/src/config/config.ts` | Centralized config with getenv() from lino-arguments |
206+
| `js/src/index.js` | Calls initAgentConfig(argv) in middleware |
207+
| `js/src/mcp/index.ts` | Uses Flag.MCP\_\*() instead of direct process.env |
208+
| `js/src/tool/read.ts` | Uses Flag.VERIFY_IMAGES_AT_READ_TOOL() |
209+
210+
## Related Issues
211+
212+
- #229 — HTTP request/response logs missing when using `--verbose` CLI flag (env var works)
213+
- #215 — Verbose HTTP logging infrastructure
214+
- #217 — Provider-level HTTP logging
215+
- #221 — Dual HTTP logging (global + provider)
216+
- #206 — Call-time verbose flag checking

experiments/issue-61/debug-provider-load.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,5 @@ console.log(' - status:', gemini3Pro.status);
5757
// Would it be filtered out?
5858
const wouldBeFiltered =
5959
(gemini3Pro.experimental || gemini3Pro.status === 'alpha') &&
60-
!process.env.OPENCODE_ENABLE_EXPERIMENTAL_MODELS;
60+
!process.env.LINK_ASSISTANT_AGENT_ENABLE_EXPERIMENTAL_MODELS;
6161
console.log(' - would be filtered:', wouldBeFiltered);

experiments/test-esm-binding.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
1-
// Test: Verify ESM live bindings work with Flag.OPENCODE_VERBOSE
2-
import { Flag } from '../js/src/flag/flag.ts';
1+
// Test: Verify ESM live bindings work with config.verbose
2+
import { config, setVerbose } from "../js/src/config/config.ts";
33

4-
console.log("Initial OPENCODE_VERBOSE:", Flag.OPENCODE_VERBOSE);
4+
console.log("Initial VERBOSE:", config.verbose);
55

6-
Flag.setVerbose(true);
7-
console.log("After setVerbose(true):", Flag.OPENCODE_VERBOSE);
6+
setVerbose(true);
7+
console.log("After setVerbose(true):", config.verbose);
88

9-
Flag.setVerbose(false);
10-
console.log("After setVerbose(false):", Flag.OPENCODE_VERBOSE);
9+
setVerbose(false);
10+
console.log("After setVerbose(false):", config.verbose);
1111

1212
// Test the exact pattern used in provider.ts
1313
const check = () => {
14-
if (!Flag.OPENCODE_VERBOSE) {
14+
if (!config.verbose) {
1515
console.log(" -> Would SKIP verbose logging");
1616
} else {
1717
console.log(" -> Would DO verbose logging");
1818
}
1919
};
2020

2121
console.log("\nWith verbose=false:");
22-
Flag.setVerbose(false);
22+
setVerbose(false);
2323
check();
2424

2525
console.log("With verbose=true:");
26-
Flag.setVerbose(true);
26+
setVerbose(true);
2727
check();
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* Reproduce the exact invocation from the broken log to check argv parsing
4+
*/
5+
import yargs from 'yargs';
6+
import { hideBin } from 'yargs/helpers';
7+
import { buildRunOptions } from '../js/src/cli/run-options.js';
8+
9+
const yargsInstance = yargs(hideBin(process.argv))
10+
.scriptName('agent')
11+
.usage('$0 [command] [options]')
12+
.version('0.18.3')
13+
.command({
14+
command: '$0',
15+
describe: 'Run agent',
16+
builder: buildRunOptions,
17+
handler: async (argv) => {
18+
console.log('[handler] argv.verbose =', argv.verbose);
19+
},
20+
})
21+
.middleware(async (argv) => {
22+
console.log('[middleware] argv.verbose =', argv.verbose);
23+
console.log('[middleware] argv.model =', argv.model);
24+
console.log('[middleware] all boolean keys:',
25+
Object.entries(argv)
26+
.filter(([k,v]) => typeof v === 'boolean')
27+
.map(([k,v]) => `${k}=${v}`)
28+
.join(', ')
29+
);
30+
});
31+
32+
await yargsInstance.parse();

experiments/test-flag-binding.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* Test if TypeScript namespace export let creates a live binding
4+
*/
5+
6+
// Simulate the Flag namespace
7+
export namespace TestFlag {
8+
export let value = false;
9+
export function setValue(v: boolean) {
10+
value = v;
11+
}
12+
}
13+
14+
// Import and test
15+
console.log('Before setValue:', TestFlag.value);
16+
TestFlag.setValue(true);
17+
console.log('After setValue:', TestFlag.value);
18+
19+
// Simulate what verbose-fetch does
20+
function checkFlag() {
21+
return TestFlag.value;
22+
}
23+
24+
console.log('checkFlag():', checkFlag());

0 commit comments

Comments
 (0)