@mafdet/cli 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +4 -6
  3. package/README.zh-CN.md +4 -3
  4. package/dist/agent/command-policy.d.ts.map +1 -1
  5. package/dist/agent/command-policy.js +10 -1
  6. package/dist/agent/context-budget.d.ts +15 -0
  7. package/dist/agent/context-budget.d.ts.map +1 -1
  8. package/dist/agent/context-budget.js +46 -6
  9. package/dist/agent/loop.d.ts +9 -1
  10. package/dist/agent/loop.d.ts.map +1 -1
  11. package/dist/agent/loop.js +21 -2
  12. package/dist/agent/session.d.ts +2 -0
  13. package/dist/agent/session.d.ts.map +1 -1
  14. package/dist/agent/session.js +7 -2
  15. package/dist/api/sse-parser.d.ts +15 -0
  16. package/dist/api/sse-parser.d.ts.map +1 -1
  17. package/dist/api/sse-parser.js +28 -1
  18. package/dist/commands/agent.d.ts.map +1 -1
  19. package/dist/commands/agent.js +75 -26
  20. package/dist/commands/sessions.d.ts.map +1 -1
  21. package/dist/commands/sessions.js +34 -8
  22. package/dist/sessions/index-file.d.ts +2 -0
  23. package/dist/sessions/index-file.d.ts.map +1 -1
  24. package/dist/sessions/index-file.js +3 -0
  25. package/dist/sessions/open.d.ts.map +1 -1
  26. package/dist/sessions/open.js +3 -1
  27. package/dist/sessions/resume.d.ts +1 -0
  28. package/dist/sessions/resume.d.ts.map +1 -1
  29. package/dist/sessions/resume.js +5 -3
  30. package/dist/tui/agent-surface.d.ts.map +1 -1
  31. package/dist/tui/agent-surface.js +10 -1
  32. package/dist/tui/app.d.ts +14 -0
  33. package/dist/tui/app.d.ts.map +1 -1
  34. package/dist/tui/app.js +45 -11
  35. package/dist/tui/picker.d.ts.map +1 -1
  36. package/dist/tui/picker.js +2 -1
  37. package/dist/tui/session-centre.d.ts +5 -0
  38. package/dist/tui/session-centre.d.ts.map +1 -1
  39. package/dist/tui/session-centre.js +9 -3
  40. package/dist/tui/start-agent.d.ts +28 -0
  41. package/dist/tui/start-agent.d.ts.map +1 -1
  42. package/dist/tui/start-agent.js +39 -9
  43. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,49 @@ The version is the contract for the JSON output shapes and the exit codes.
6
6
  Being 0.x, semver still permits a break in a minor bump; the intent is not to
7
7
  use that latitude, and the promise becomes binding at 1.0.0.
8
8
 
9
+ ## 0.4.5 - 2026-08-12
10
+
11
+ A second audit round, and the security fix in it is the important one.
12
+
13
+ **With no tool-calling model, the interactive session no longer opens.** It
14
+ used to print a warning and then offer every chat model - so the one case with
15
+ nothing safe to offer was the case that offered everything, and the agent then
16
+ ran on a model whose tool calling had never been verified. `mafdet agent`
17
+ refused the same model outright, so the two surfaces disagreed about
18
+ admission. They no longer do.
19
+
20
+ **A cost ceiling that cannot be enforced stops the run.** When the catalogue
21
+ has no price for a model, each turn counted as zero towards `--max-cost-usd`,
22
+ so the ceiling quietly stopped being one while the run kept spending. Asking
23
+ for a limit we cannot apply now ends the run and says why.
24
+
25
+ **Context compaction keeps going until the history fits.** It used to drop a
26
+ single tool exchange and send regardless, which changes nothing when the
27
+ history is several times over the window - the request then failed upstream
28
+ and was billed for the attempt.
29
+
30
+ **A stream is closed when the answer ends.** `[DONE]` means the reply is
31
+ complete, not that the socket is; an upstream that kept the connection open
32
+ held ours too. There is also a total deadline now, because a trickle of bytes
33
+ defeats an idle one.
34
+
35
+ **More of what reaches the terminal is sanitised**: the answer from `mafdet
36
+ agent`, the interactive session's answer, the session picker, the resume
37
+ notice and the remaining `sessions` columns. The stripper also handles the
38
+ 8-bit C1 forms, which do the same things without an escape character.
39
+
40
+ **An unpriced request is no longer displayed as `$0.00`** in `sessions list`,
41
+ `show` or `usage`, and the JSON carries `unpriced_requests` so a script can
42
+ tell the difference too.
43
+
44
+ Also: `sessions list` refuses stray arguments like the other subcommands;
45
+ `mafdet agent` documents `--title`, `--no-history` and `--read-only`, shows
46
+ the first-save notice, and closes its recorder even when the session throws;
47
+ resuming no longer asks you to pick a model it is going to ignore, and the
48
+ banner names the model actually in use; and the Windows command-timeout note
49
+ now says the `taskkill` path is written but unverified, rather than describing
50
+ the implementation it replaced.
51
+
9
52
  ## 0.4.4 - 2026-08-11
10
53
 
11
54
  A security fix and twenty-one other corrections, all from an external audit of
package/README.md CHANGED
@@ -3,13 +3,11 @@
3
3
  The official Mafdet AI command line: model calls, account queries, and a
4
4
  permissioned local agent.
5
5
 
6
- > **0.4.4.** 中文版见 [README.zh-CN.md](./README.zh-CN.md). The CLI never
6
+ > **0.4.5.** 中文版见 [README.zh-CN.md](./README.zh-CN.md). The CLI never
7
7
  > advertises a command it cannot serve, so `mafdet help` always lists exactly
8
8
  > what works.
9
9
  >
10
- > macOS and Windows are not yet verified. On Windows the command timeout
11
- > relies on POSIX process groups, so only the direct child is terminated and a
12
- > grandchild can survive.
10
+ > macOS and Windows are not yet verified. On Windows the command timeout uses `taskkill /T`, which is written but has never been run on a real Windows machine - so whether every grandchild process is reached there is unverified rather than fixed.
13
11
 
14
12
  ## Requirements
15
13
 
@@ -25,8 +23,8 @@ Or build it from this repository:
25
23
 
26
24
  ```bash
27
25
  pnpm --filter @mafdet/cli build
28
- cd packages/cli && npm pack # -> mafdet-cli-0.4.4.tgz
29
- npm install -g ./mafdet-cli-0.4.4.tgz
26
+ cd packages/cli && npm pack # -> mafdet-cli-0.4.5.tgz
27
+ npm install -g ./mafdet-cli-0.4.5.tgz
30
28
  ```
31
29
 
32
30
  ## The interactive session
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Mafdet AI 官方命令行:模型调用、账户查询,以及一个受权限约束的本地 Agent。
4
4
 
5
- > **0.4.4。** 英文版见 [README.md](./README.md)。
5
+ > **0.4.5。** 英文版见 [README.md](./README.md)。
6
6
 
7
7
  ## 环境要求
8
8
 
@@ -55,8 +55,9 @@ mafdet doctor
55
55
  的默认状态,中文系统为 cp936)也不会变成乱码。管道传入 prompt 也可以,但 PowerShell
56
56
  默认按 UTF-16 走管道——建议直接把 prompt 作为参数,或用 `--input <文件>`。
57
57
 
58
- ⚠️ **macOS 与 Windows 尚未验收**。Windows 上有一个已知差异:命令超时终止依赖 POSIX 的
59
- 进程组语义,Windows 只会终止直接子进程,孙子进程可能存活。
58
+ ⚠️ **macOS 与 Windows 尚未验收**。Windows 上的命令超时终止已改用 `taskkill /T`,
59
+ 但**从未在真 Windows 上执行过**——它是否真的能杀到每一个孙子进程,属于**未验证**,
60
+ 不是「已修复」。
60
61
 
61
62
  ## 命令
62
63
 
@@ -1 +1 @@
1
- {"version":3,"file":"command-policy.d.ts","sourceRoot":"","sources":["../../src/agent/command-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAeH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CA4BlD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,SAAS,GAAE,SAAS,MAAM,EAAO,GAChC,cAAc,CAwChB;AAmBD,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAe9C"}
1
+ {"version":3,"file":"command-policy.d.ts","sourceRoot":"","sources":["../../src/agent/command-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAeH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CA4BlD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,SAAS,GAAE,SAAS,MAAM,EAAO,GAChC,cAAc,CAwChB;AAmBD,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAwB9C"}
@@ -129,5 +129,14 @@ export function stripAnsi(text) {
129
129
  // Remaining C0 controls. Tab, newline and carriage return survive; the
130
130
  // rest can retitle a window, move the cursor or hide what follows, and a
131
131
  // model's output is not a trusted source of terminal instructions.
132
- .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, ""));
132
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "")
133
+ // R-4 · The 8-bit C1 forms of the same sequences. A terminal in 8-bit
134
+ // mode reads U+009B as CSI and U+009D as OSC - identical instructions to
135
+ // the ESC forms above, with no ESC to match on. Taken as whole
136
+ // sequences, so the payload goes with the introducer rather than being
137
+ // left behind as visible text.
138
+ .replace(/\u009b[0-?]*[ -/]*[@-~]/g, "")
139
+ .replace(/\u009d[^\u0007\u009c]*(?:\u0007|\u009c)?/g, "")
140
+ // Any remaining C1, including NEL (U+0085) and the lone introducers.
141
+ .replace(/[\u0080-\u009f]/g, ""));
133
142
  }
@@ -66,4 +66,19 @@ export interface CompactionResult extends PruneResult {
66
66
  * upstream and be billed for the attempt.
67
67
  */
68
68
  export declare function compactForBudget(history: ChatMessage[], keepRecent?: number): CompactionResult;
69
+ /**
70
+ * R-2 · Shorten until it fits, or until nothing more can go.
71
+ *
72
+ * `compactForBudget` used to return the moment it dropped a single tool
73
+ * exchange, and the caller then sent the request regardless. One exchange out
74
+ * of a history three times over the window changes nothing, so the request
75
+ * still failed upstream - and was billed for the attempt.
76
+ *
77
+ * `estimate` maps the current history back to a token count. There is no
78
+ * tokenizer here (see the note at the top of this file), so it is derived from
79
+ * the last measurement: tokens per message, applied to what is left. Rough, but
80
+ * it is the only honest figure available and it is monotone in the right
81
+ * direction, which is what a loop needs.
82
+ */
83
+ export declare function compactUntilItFits(history: ChatMessage[], contextLength: number | undefined, lastPromptTokens: number, keepRecent?: number): CompactionResult;
69
84
  //# sourceMappingURL=context-budget.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context-budget.d.ts","sourceRoot":"","sources":["../../src/agent/context-budget.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,OAAO,CAAC;AAE/B,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,6EAA6E;AAC7E,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAKT;AAED,MAAM,WAAW,WAAW;IAC1B,uCAAuC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAsCD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EAAE,EACtB,UAAU,GAAE,MAA8B,GACzC,WAAW,CAkBb;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,WAAW,EAAE,EACtB,SAAS,EAAE,MAAM,GAChB,MAAM,CAuBR;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,WAAW,EAAE,EACtB,UAAU,GAAE,MAA8B,GACzC,gBAAgB,CAYlB"}
1
+ {"version":3,"file":"context-budget.d.ts","sourceRoot":"","sources":["../../src/agent/context-budget.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,OAAO,CAAC;AAE/B,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,6EAA6E;AAC7E,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAKT;AAED,MAAM,WAAW,WAAW;IAC1B,uCAAuC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAsCD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EAAE,EACtB,UAAU,GAAE,MAA8B,GACzC,WAAW,CAkBb;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,WAAW,EAAE,EACtB,SAAS,EAAE,MAAM,GAChB,MAAM,CAuBR;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,WAAW,EAAE,EACtB,UAAU,GAAE,MAA8B,GACzC,gBAAgB,CASlB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EAAE,EACtB,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,gBAAgB,EAAE,MAAM,EACxB,UAAU,GAAE,MAA8B,GACzC,gBAAgB,CA6BlB"}
@@ -156,14 +156,54 @@ export function dropOldestTurns(history, keepTurns) {
156
156
  */
157
157
  export function compactForBudget(history, keepRecent = KEEP_RECENT_EXCHANGES) {
158
158
  const pruned = pruneToolExchanges(history, keepRecent);
159
- if (pruned.exchanges > 0) {
160
- return { ...pruned, turns: 0, exhausted: false };
161
- }
162
159
  const turnsRemoved = dropOldestTurns(history, keepRecent);
163
160
  return {
164
- removed: turnsRemoved,
165
- exchanges: 0,
161
+ removed: pruned.removed + turnsRemoved,
162
+ exchanges: pruned.exchanges,
166
163
  turns: turnsRemoved > 0 ? 1 : 0,
167
- exhausted: turnsRemoved === 0,
164
+ exhausted: pruned.exchanges === 0 && turnsRemoved === 0,
168
165
  };
169
166
  }
167
+ /**
168
+ * R-2 · Shorten until it fits, or until nothing more can go.
169
+ *
170
+ * `compactForBudget` used to return the moment it dropped a single tool
171
+ * exchange, and the caller then sent the request regardless. One exchange out
172
+ * of a history three times over the window changes nothing, so the request
173
+ * still failed upstream - and was billed for the attempt.
174
+ *
175
+ * `estimate` maps the current history back to a token count. There is no
176
+ * tokenizer here (see the note at the top of this file), so it is derived from
177
+ * the last measurement: tokens per message, applied to what is left. Rough, but
178
+ * it is the only honest figure available and it is monotone in the right
179
+ * direction, which is what a loop needs.
180
+ */
181
+ export function compactUntilItFits(history, contextLength, lastPromptTokens, keepRecent = KEEP_RECENT_EXCHANGES) {
182
+ const startedWith = history.length;
183
+ const perMessage = startedWith > 0 ? lastPromptTokens / startedWith : 0;
184
+ let exchanges = 0;
185
+ let turns = 0;
186
+ for (;;) {
187
+ const estimate = Math.round(perMessage * history.length);
188
+ if (!shouldCompact(contextLength, estimate)) {
189
+ return {
190
+ removed: startedWith - history.length,
191
+ exchanges,
192
+ turns,
193
+ exhausted: false,
194
+ };
195
+ }
196
+ const step = compactForBudget(history, keepRecent);
197
+ if (step.exhausted) {
198
+ return {
199
+ removed: startedWith - history.length,
200
+ exchanges,
201
+ turns,
202
+ // Still over budget with nothing left that is safe to drop.
203
+ exhausted: true,
204
+ };
205
+ }
206
+ exchanges += step.exchanges;
207
+ turns += step.turns;
208
+ }
209
+ }
@@ -14,7 +14,13 @@ export declare const DEFAULT_LIMITS: {
14
14
  export type AgentLimits = typeof DEFAULT_LIMITS;
15
15
  export type StopReason = "completed" | "max_turns" | "timeout" | "max_cost" | "cancelled" | "rate_limited"
16
16
  /** F-008 · Over the model's window with nothing left that is safe to drop. */
17
- | "context_limit" | "error";
17
+ | "context_limit"
18
+ /**
19
+ * R-5 · A cost ceiling was asked for and cannot be honoured, because the
20
+ * catalogue has no price for this model. Distinct from `max_cost`, which
21
+ * means the ceiling was reached.
22
+ */
23
+ | "cost_unknown" | "error";
18
24
  export interface AgentReport {
19
25
  stopReason: StopReason;
20
26
  /** The model's last text answer, if it produced one. */
@@ -122,6 +128,8 @@ export type AgentEvent = {
122
128
  | {
123
129
  type: "compacted";
124
130
  exchanges: number;
131
+ /** Whole conversation turns dropped, when exchanges were not enough. */
132
+ turns: number;
125
133
  messages: number;
126
134
  promptTokens: number;
127
135
  };
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { ApiError } from "../api/errors.js";
2
- import { compactForBudget, shouldCompact } from "./context-budget.js";
2
+ import { compactUntilItFits, shouldCompact } from "./context-budget.js";
3
3
  import { toolToJsonSchema, } from "./tools/types.js";
4
4
  /** Plan §7.4. */
5
5
  export const DEFAULT_LIMITS = {
@@ -47,6 +47,8 @@ export async function runAgent(options) {
47
47
  };
48
48
  /** What the provider counted for the previous request; 0 until one lands. */
49
49
  let lastPromptTokens = 0;
50
+ /** R-5 · Turns whose cost the catalogue could not tell us. */
51
+ let unpricedTurns = 0;
50
52
  for (let turn = 1; turn <= limits.maxTurns; turn += 1) {
51
53
  if (options.signal?.aborted)
52
54
  return stop(report, "cancelled");
@@ -56,11 +58,23 @@ export async function runAgent(options) {
56
58
  report.costMicroUsd >= limits.maxCostMicroUsd) {
57
59
  return stop(report, "max_cost");
58
60
  }
61
+ // R-5 · An unpriceable turn counted as 0 towards the ceiling, so
62
+ // `--max-cost-usd` silently stopped being a ceiling at all and the run
63
+ // continued spending real money. A limit that cannot be enforced has to
64
+ // stop the run, not be quietly abandoned.
65
+ if (limits.maxCostMicroUsd !== undefined && unpricedTurns > 0) {
66
+ return stop(report, "cost_unknown", `--max-cost-usd cannot be enforced: the catalogue has no price for ` +
67
+ `${options.model}, so ${unpricedTurns} request(s) could not be counted ` +
68
+ `towards the ceiling. Re-run without the limit to continue anyway.`);
69
+ }
59
70
  report.turns = turn;
60
71
  options.onEvent?.({ type: "turn", turn });
61
72
  // §7.4 · Shorten before asking, using what the provider counted last time.
62
73
  if (shouldCompact(options.pricing?.contextLength, lastPromptTokens)) {
63
- const compacted = compactForBudget(messages);
74
+ // R-2 · Keep going until the estimate is inside the budget. Dropping one
75
+ // exchange and sending anyway left a history that was still over the
76
+ // window: the request failed upstream and was billed for the attempt.
77
+ const compacted = compactUntilItFits(messages, options.pricing?.contextLength, lastPromptTokens);
64
78
  if (compacted.exhausted) {
65
79
  // F-008 · Nothing left that can be dropped without breaking the
66
80
  // conversation. Sending anyway means a request over the window, which
@@ -72,6 +86,9 @@ export async function runAgent(options) {
72
86
  options.onEvent?.({
73
87
  type: "compacted",
74
88
  exchanges: compacted.exchanges,
89
+ // R-2 · Reported separately: a pure conversation drops turns and no
90
+ // exchanges, and the display said "dropped 0 earlier tool exchanges".
91
+ turns: compacted.turns,
75
92
  messages: compacted.removed,
76
93
  promptTokens: lastPromptTokens,
77
94
  });
@@ -116,6 +133,8 @@ export async function runAgent(options) {
116
133
  costMicroUsd: turnCost,
117
134
  priced: canPrice(options.pricing, usage),
118
135
  });
136
+ if (!canPrice(options.pricing, usage))
137
+ unpricedTurns += 1;
119
138
  const choice = response.choices?.[0];
120
139
  const message = choice?.message;
121
140
  const calls = message?.tool_calls ?? [];
@@ -32,6 +32,8 @@ export interface SessionOptions {
32
32
  * the same events are drawn.
33
33
  */
34
34
  onEvent?: (event: AgentEvent) => void;
35
+ /** R-4 · False when stdout is a pipe or a file, which get exact bytes. */
36
+ isTty?: boolean;
35
37
  /**
36
38
  * A conversation to continue, rebuilt from a saved session (CLI-024).
37
39
  * Appended to in place, exactly as the fresh array would be.
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/agent/session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAY,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAapE,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,oEAAoE;IACpE,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,cAAc,CAAC,CA8FzB;AAgHD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,CA2B7D"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/agent/session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,OAAO,EAAY,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAapE,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,oEAAoE;IACpE,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC,0EAA0E;IAC1E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,cAAc,CAAC,CAqGzB;AAgHD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,CA2B7D"}
@@ -2,6 +2,7 @@ import { createInterface } from "node:readline/promises";
2
2
  import { isAgentEligible } from "../api/models.js";
3
3
  import { formatUsd } from "../output/money.js";
4
4
  import { renderRecord } from "../output/format.js";
5
+ import { safeForTerminal } from "../output/terminal-safe.js";
5
6
  import { runAgent } from "./loop.js";
6
7
  const HELP = `Session commands:
7
8
  /model [id] Show or switch the model
@@ -94,8 +95,12 @@ export async function runSession(options) {
94
95
  summary.promptTokens += report.promptTokens;
95
96
  summary.completionTokens += report.completionTokens;
96
97
  summary.costMicroUsd += report.costMicroUsd;
97
- if (report.finalText)
98
- options.stdout(`${report.finalText}\n`);
98
+ // R-4 · The model's answer reaches a terminal here too. The TUI wraps
99
+ // this writer, but `mafdet agent` without a goal does not, so the
100
+ // sanitising belongs at the sink rather than at one of its callers.
101
+ if (report.finalText) {
102
+ options.stdout(`${safeForTerminal(report.finalText, options.isTty ?? true)}\n`);
103
+ }
99
104
  if (report.stopReason !== "completed") {
100
105
  options.stderr(`[${report.stopReason}${report.error ? `: ${report.error}` : ""}]\n`);
101
106
  }
@@ -72,6 +72,19 @@ export interface StreamLimits {
72
72
  readonly idleTimeoutMs?: number;
73
73
  /** Total bytes accepted before the stream is treated as hostile. */
74
74
  readonly maxBytes?: number;
75
+ /**
76
+ * R-3 · A ceiling on the whole stream, not just on the gaps in it.
77
+ *
78
+ * The idle deadline is the right primitive for "this hung" - a long
79
+ * generation is not a stalled one - but it is defeated by a trickle: a few
80
+ * bytes before each deadline holds the connection indefinitely. The byte cap
81
+ * bounds the volume and not the time, so this bounds the time.
82
+ *
83
+ * Generous on purpose. It is a backstop against a hostile upstream, not a
84
+ * limit on how long a model may think, and a single response that takes
85
+ * longer than this is not a case worth serving.
86
+ */
87
+ readonly maxTotalMs?: number;
75
88
  /**
76
89
  * Closes the underlying connection. Cancelling the reader alone stops us
77
90
  * reading; it does not reliably stop the gateway generating, and generation
@@ -81,6 +94,8 @@ export interface StreamLimits {
81
94
  }
82
95
  export declare const DEFAULT_STREAM_IDLE_MS = 120000;
83
96
  export declare const DEFAULT_STREAM_MAX_BYTES: number;
97
+ /** Matches the agent's wall-clock ceiling (§7.4), for the same reason. */
98
+ export declare const DEFAULT_STREAM_TOTAL_MS: number;
84
99
  /**
85
100
  * Read a streaming response to completion.
86
101
  *
@@ -1 +1 @@
1
- {"version":3,"file":"sse-parser.d.ts","sourceRoot":"","sources":["../../src/api/sse-parser.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,oCAAoC;IACpC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;CACf;AAWD,+EAA+E;AAC/E,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAE7C,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,EAAE;IAMjC,kDAAkD;IAClD,GAAG,IAAI,MAAM,EAAE;IASf,OAAO,CAAC,KAAK;CAYd;AAED,8DAA8D;AAC9D,qBAAa,mBAAmB;IAOlB,OAAO,CAAC,QAAQ,CAAC,MAAM;IANnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuC;IAC7D,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,YAAY,CAAC,CAAS;IAC9B,OAAO,CAAC,KAAK,CAAC,CAAc;IAC5B,OAAO,CAAC,IAAI,CAAS;gBAEQ,MAAM,GAAE,YAAiB;IAEtD,4DAA4D;IAC5D,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAgD9B,MAAM,IAAI,YAAY;CASvB;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,eAAO,MAAM,sBAAsB,SAAU,CAAC;AAC9C,eAAO,MAAM,wBAAwB,QAAmB,CAAC;AAEzD;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,QAAQ,EAClB,MAAM,GAAE,YAAiB,EACzB,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,GAAE,YAAiB,GACxB,OAAO,CAAC,YAAY,CAAC,CA8DvB"}
1
+ {"version":3,"file":"sse-parser.d.ts","sourceRoot":"","sources":["../../src/api/sse-parser.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,oCAAoC;IACpC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;CACf;AAWD,+EAA+E;AAC/E,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAE7C,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,EAAE;IAMjC,kDAAkD;IAClD,GAAG,IAAI,MAAM,EAAE;IASf,OAAO,CAAC,KAAK;CAYd;AAED,8DAA8D;AAC9D,qBAAa,mBAAmB;IAOlB,OAAO,CAAC,QAAQ,CAAC,MAAM;IANnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuC;IAC7D,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,YAAY,CAAC,CAAS;IAC9B,OAAO,CAAC,KAAK,CAAC,CAAc;IAC5B,OAAO,CAAC,IAAI,CAAS;gBAEQ,MAAM,GAAE,YAAiB;IAEtD,4DAA4D;IAC5D,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAgD9B,MAAM,IAAI,YAAY;CASvB;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,eAAO,MAAM,sBAAsB,SAAU,CAAC;AAC9C,eAAO,MAAM,wBAAwB,QAAmB,CAAC;AACzD,0EAA0E;AAC1E,eAAO,MAAM,uBAAuB,QAAc,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,QAAQ,EAClB,MAAM,GAAE,YAAiB,EACzB,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,GAAE,YAAiB,GACxB,OAAO,CAAC,YAAY,CAAC,CAyFvB"}
@@ -102,6 +102,8 @@ export class ChatStreamAssembler {
102
102
  }
103
103
  export const DEFAULT_STREAM_IDLE_MS = 120_000;
104
104
  export const DEFAULT_STREAM_MAX_BYTES = 32 * 1024 * 1024;
105
+ /** Matches the agent's wall-clock ceiling (§7.4), for the same reason. */
106
+ export const DEFAULT_STREAM_TOTAL_MS = 30 * 60_000;
105
107
  /**
106
108
  * Read a streaming response to completion.
107
109
  *
@@ -122,9 +124,13 @@ export async function readChatStream(response, events = {}, signal, limits = {})
122
124
  return assembler.result();
123
125
  const idleMs = limits.idleTimeoutMs ?? DEFAULT_STREAM_IDLE_MS;
124
126
  const maxBytes = limits.maxBytes ?? DEFAULT_STREAM_MAX_BYTES;
127
+ const totalMs = limits.maxTotalMs ?? DEFAULT_STREAM_TOTAL_MS;
128
+ const startedAt = Date.now();
125
129
  let received = 0;
126
130
  const reader = body.getReader();
127
131
  const onAbort = () => void reader.cancel();
132
+ /** Set when `[DONE]` ended the answer while bytes may still be coming. */
133
+ let finished = false;
128
134
  // Attaching a listener is not enough: a signal that aborted between the
129
135
  // request and here would never fire one, and the read below would hang
130
136
  // forever on a stream nobody is going to close.
@@ -138,6 +144,15 @@ export async function readChatStream(response, events = {}, signal, limits = {})
138
144
  for (;;) {
139
145
  if (signal?.aborted)
140
146
  break;
147
+ if (Date.now() - startedAt > totalMs) {
148
+ limits.abort?.();
149
+ await reader.cancel().catch(() => { });
150
+ throw new ApiError({
151
+ kind: "timeout",
152
+ exitCode: ExitCode.Unavailable,
153
+ message: `the stream ran for longer than ${totalMs}ms`,
154
+ });
155
+ }
141
156
  const chunk = await withIdleDeadline(reader.read(), idleMs);
142
157
  if (chunk === IDLE) {
143
158
  limits.abort?.();
@@ -162,8 +177,14 @@ export async function readChatStream(response, events = {}, signal, limits = {})
162
177
  });
163
178
  }
164
179
  for (const payload of framer.push(value)) {
165
- if (assembler.push(payload))
180
+ if (assembler.push(payload)) {
181
+ // R-3 · `[DONE]` means the answer is complete, not that the socket
182
+ // is. An upstream that keeps the connection open after it would
183
+ // otherwise hold ours too - releasing the reader lock in the
184
+ // `finally` does not close anything.
185
+ finished = true;
166
186
  return assembler.result();
187
+ }
167
188
  }
168
189
  }
169
190
  for (const payload of framer.end())
@@ -171,6 +192,12 @@ export async function readChatStream(response, events = {}, signal, limits = {})
171
192
  }
172
193
  finally {
173
194
  signal?.removeEventListener("abort", onAbort);
195
+ if (finished) {
196
+ // Cancel first, then release: cancelling closes the body, and the abort
197
+ // reaches the fetch itself, which is what actually frees the socket.
198
+ void reader.cancel().catch(() => { });
199
+ limits.abort?.();
200
+ }
174
201
  reader.releaseLock();
175
202
  }
176
203
  return assembler.result();
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/commands/agent.ts"],"names":[],"mappings":"AAKA,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAYvE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAatB,eAAO,MAAM,gBAAgB,0XAK+C,CAAC;AAE7E,eAAO,MAAM,YAAY,mgBAOmB,CAAC;AAE7C,MAAM,WAAW,OAAQ,SAAQ,SAAS;IACxC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,qDAAqD;IACrD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,uBAAuB,EAAE,QAAQ,CAAC;CACrD;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,QAAQ,CAAC,CAsHnB;AAyJD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,EAAE,kBAAkB,EAAE,OAAO,0JAI3E"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/commands/agent.ts"],"names":[],"mappings":"AAQA,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAiBvE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAmBtB,eAAO,MAAM,gBAAgB,0XAK+C,CAAC;AAE7E,eAAO,MAAM,YAAY,mgBAOmB,CAAC;AAE7C,MAAM,WAAW,OAAQ,SAAQ,SAAS;IACxC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,qDAAqD;IACrD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,uBAAuB,EAAE,QAAQ,CAAC;CACrD;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,QAAQ,CAAC,CA6HnB;AAoLD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,EAAE,kBAAkB,EAAE,OAAO,0JAI3E"}
@@ -1,10 +1,13 @@
1
1
  import { fetchAccessibleModels, fetchCatalog, intersectModels, } from "../api/models.js";
2
+ import { existsSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
2
4
  import { approverFor } from "../agent/approvals.js";
3
5
  import { runAgent } from "../agent/loop.js";
4
6
  import { runSession } from "../agent/session.js";
7
+ import { safeForTerminal } from "../output/terminal-safe.js";
5
8
  import { sessionsDir } from "../config/paths.js";
6
9
  import { keyDigest } from "../security/redact.js";
7
- import { decideRecording } from "../sessions/policy.js";
10
+ import { decideRecording, firstSaveNotice, NOTICE_FILE, } from "../sessions/policy.js";
8
11
  import { SessionRecorder } from "../sessions/recorder.js";
9
12
  import { SessionWriter } from "../sessions/store.js";
10
13
  import { READ_ONLY_TOOLS } from "../agent/tools/read-only.js";
@@ -16,14 +19,20 @@ import { renderJson, renderRecord } from "../output/format.js";
16
19
  import { formatUsd } from "../output/money.js";
17
20
  import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
18
21
  const USAGE = `Usage:
19
- mafdet agent "<goal>" [--model <id>] [--allow-write]
22
+ mafdet agent "<goal>" [--model <id>] [--allow-write] [--read-only]
20
23
  [--allow-command "<prefix>"]... [--max-turns <n>]
21
24
  [--max-cost-usd <n>] [--json]
25
+ mafdet agent Interactive session (needs a terminal)
26
+ [--title <text>] [--no-history]
22
27
 
23
28
  Read-only by default. With --allow-write the agent may edit and create files,
24
- and every change is shown as a diff and confirmed before it is written. There
25
- is no flag that approves everything in advance, and a non-interactive run
26
- cannot write at all.
29
+ and every change is shown as a diff and confirmed before it is written.
30
+ --read-only cancels an earlier --allow-write. There is no flag that approves
31
+ everything in advance, and a non-interactive run cannot write at all.
32
+
33
+ An interactive session is saved on this computer; --no-history records
34
+ nothing and --title names it in 'mafdet sessions list'. To continue a saved
35
+ conversation, use 'mafdet --resume <id>'.
27
36
  `;
28
37
  export const READ_ONLY_PROMPT = `You are the Mafdet CLI agent working inside a user's workspace.
29
38
 
@@ -82,25 +91,33 @@ export async function agentCommand(argv, io) {
82
91
  // F-007 · `decideRecording` already called this surface "interactive",
83
92
  // so the policy claimed these runs were saved while nothing saved them.
84
93
  const recorder = openRecorder(parsed, io, model, workspace.root, mode);
85
- const summary = await runSession({
86
- client: runtime.dataPlane,
87
- ...(recorder === undefined
88
- ? {}
89
- : { onEvent: (event) => recorder.observe(event) }),
90
- model,
91
- pricing: resolved,
92
- catalog,
93
- systemPrompt: mode === "read-only" ? READ_ONLY_PROMPT : WRITE_PROMPT,
94
- tools: buildToolset(mode, parsed.allowedCommands.length > 0),
95
- toolContext: { workspace, allowedCommands: parsed.allowedCommands },
96
- approver: io.approver ?? approverFor(mode, io, interactive),
97
- mode,
98
- allowedCommands: parsed.allowedCommands,
99
- stdout: io.stdout,
100
- stderr: io.stderr,
101
- signal: io.signal,
102
- });
103
- recorder?.close();
94
+ // R-8 · In a finally: `close()` is what writes the final index entry, so
95
+ // a session that threw left the listing without it.
96
+ let summary;
97
+ try {
98
+ summary = await runSession({
99
+ client: runtime.dataPlane,
100
+ ...(recorder === undefined
101
+ ? {}
102
+ : { onEvent: (event) => recorder.observe(event) }),
103
+ model,
104
+ pricing: resolved,
105
+ catalog,
106
+ systemPrompt: mode === "read-only" ? READ_ONLY_PROMPT : WRITE_PROMPT,
107
+ tools: buildToolset(mode, parsed.allowedCommands.length > 0),
108
+ toolContext: { workspace, allowedCommands: parsed.allowedCommands },
109
+ approver: io.approver ?? approverFor(mode, io, interactive),
110
+ mode,
111
+ allowedCommands: parsed.allowedCommands,
112
+ stdout: io.stdout,
113
+ stderr: io.stderr,
114
+ signal: io.signal,
115
+ isTty: isTty(io),
116
+ });
117
+ }
118
+ finally {
119
+ recorder?.close();
120
+ }
104
121
  io.stderr("\n" +
105
122
  renderRecord([
106
123
  ["goals", `${summary.goals}`],
@@ -149,6 +166,29 @@ export async function agentCommand(argv, io) {
149
166
  * writes nothing at all, and a store that cannot be opened costs the
150
167
  * transcript rather than the session.
151
168
  */
169
+ /**
170
+ * R-8 · The first-save notice, once per machine.
171
+ *
172
+ * The marker file is shared with the TUI on purpose: the notice is about this
173
+ * computer, not about which command happened to write first.
174
+ */
175
+ function showNoticeOnce(root, io) {
176
+ const marker = join(root, NOTICE_FILE);
177
+ try {
178
+ if (existsSync(marker))
179
+ return;
180
+ io.stderr(firstSaveNotice(root));
181
+ writeFileSync(marker, `${new Date().toISOString()}\n`, { mode: 0o600 });
182
+ }
183
+ catch {
184
+ // Telling the user is best effort; failing to record that we told them
185
+ // must not stop the session.
186
+ }
187
+ }
188
+ /** Whether a terminal is on the other end of stdout. Injectable for tests. */
189
+ function isTty(io) {
190
+ return io.isTty ?? process.stdout.isTTY === true;
191
+ }
152
192
  function openRecorder(parsed, io, modelId, workspaceRoot, mode) {
153
193
  const decision = decideRecording({
154
194
  surface: "interactive",
@@ -169,6 +209,10 @@ function openRecorder(parsed, io, modelId, workspaceRoot, mode) {
169
209
  workspace: workspaceRoot,
170
210
  mode,
171
211
  });
212
+ // R-8 · Said once per machine, on whichever surface records first. The
213
+ // TUI showed it and this one did not, so a `mafdet agent` user could have
214
+ // a transcript on disk without ever being told.
215
+ showNoticeOnce(root, io);
172
216
  return new SessionRecorder({
173
217
  writer,
174
218
  root,
@@ -303,8 +347,10 @@ function finish(report, json, io) {
303
347
  io.stdout(renderJson({ ...report, costMicroUsd: report.costMicroUsd.toString() }));
304
348
  }
305
349
  else {
306
- if (report.finalText)
307
- io.stdout(`${report.finalText}\n`);
350
+ // R-4 · The model's answer, sanitised only when a terminal is reading it.
351
+ if (report.finalText) {
352
+ io.stdout(`${safeForTerminal(report.finalText, isTty(io))}\n`);
353
+ }
308
354
  io.stderr("\n" +
309
355
  renderRecord([
310
356
  ["status", report.stopReason],
@@ -334,6 +380,9 @@ function finish(report, json, io) {
334
380
  case "max_cost":
335
381
  // F-008 · A ceiling too: the window ran out, and the report says which.
336
382
  case "context_limit":
383
+ // R-5 · Asked for a ceiling we cannot enforce; stopped rather than spend
384
+ // past it. A limit, so it belongs with the other limits.
385
+ case "cost_unknown":
337
386
  // Stopped short on purpose; the report says so rather than pretending.
338
387
  return ExitCode.LimitReached;
339
388
  case "rate_limited":
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/commands/sessions.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AA4BnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAgBtB,wBAAsB,eAAe,CACnC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,SAAS,GACZ,OAAO,CAAC,QAAQ,CAAC,CAUnB;AA8PD,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,KAAG,MAUX,CAAC"}
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/commands/sessions.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AA4BnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAgBtB,wBAAsB,eAAe,CACnC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,SAAS,GACZ,OAAO,CAAC,QAAQ,CAAC,CAUnB;AAoRD,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,KAAG,MAUX,CAAC"}