@mafdet/cli 0.4.4 → 0.4.6

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 (48) hide show
  1. package/CHANGELOG.md +81 -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 +19 -6
  6. package/dist/agent/context-budget.d.ts +19 -1
  7. package/dist/agent/context-budget.d.ts.map +1 -1
  8. package/dist/agent/context-budget.js +87 -12
  9. package/dist/agent/loop.d.ts +16 -1
  10. package/dist/agent/loop.d.ts.map +1 -1
  11. package/dist/agent/loop.js +34 -2
  12. package/dist/agent/session.d.ts +14 -0
  13. package/dist/agent/session.d.ts.map +1 -1
  14. package/dist/agent/session.js +15 -4
  15. package/dist/api/models.d.ts.map +1 -1
  16. package/dist/api/models.js +28 -2
  17. package/dist/api/sse-parser.d.ts +15 -0
  18. package/dist/api/sse-parser.d.ts.map +1 -1
  19. package/dist/api/sse-parser.js +39 -3
  20. package/dist/commands/agent.d.ts.map +1 -1
  21. package/dist/commands/agent.js +88 -30
  22. package/dist/commands/sessions.d.ts.map +1 -1
  23. package/dist/commands/sessions.js +45 -10
  24. package/dist/output/money.d.ts +9 -0
  25. package/dist/output/money.d.ts.map +1 -1
  26. package/dist/output/money.js +14 -0
  27. package/dist/sessions/index-file.d.ts +2 -0
  28. package/dist/sessions/index-file.d.ts.map +1 -1
  29. package/dist/sessions/index-file.js +3 -0
  30. package/dist/sessions/open.d.ts.map +1 -1
  31. package/dist/sessions/open.js +3 -1
  32. package/dist/sessions/resume.d.ts +1 -0
  33. package/dist/sessions/resume.d.ts.map +1 -1
  34. package/dist/sessions/resume.js +5 -3
  35. package/dist/tui/agent-surface.d.ts.map +1 -1
  36. package/dist/tui/agent-surface.js +18 -12
  37. package/dist/tui/app.d.ts +14 -0
  38. package/dist/tui/app.d.ts.map +1 -1
  39. package/dist/tui/app.js +45 -11
  40. package/dist/tui/picker.d.ts.map +1 -1
  41. package/dist/tui/picker.js +2 -1
  42. package/dist/tui/session-centre.d.ts +5 -0
  43. package/dist/tui/session-centre.d.ts.map +1 -1
  44. package/dist/tui/session-centre.js +14 -4
  45. package/dist/tui/start-agent.d.ts +28 -0
  46. package/dist/tui/start-agent.d.ts.map +1 -1
  47. package/dist/tui/start-agent.js +39 -9
  48. package/package.json +1 -1
@@ -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,CAqGvB"}
@@ -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,14 +144,32 @@ export async function readChatStream(response, events = {}, signal, limits = {})
138
144
  for (;;) {
139
145
  if (signal?.aborted)
140
146
  break;
141
- const chunk = await withIdleDeadline(reader.read(), idleMs);
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
+ }
156
+ // T-6 · Whichever runs out first. Checking the total before the read
157
+ // and then waiting a whole idle period overshot the total by up to one
158
+ // of them - with a 30 minute total and a 2 minute idle that is a
159
+ // rounding error, but with the total below the idle it never applied
160
+ // at all.
161
+ const remaining = totalMs - (Date.now() - startedAt);
162
+ const chunk = await withIdleDeadline(reader.read(), Math.max(0, Math.min(idleMs, remaining)));
142
163
  if (chunk === IDLE) {
143
164
  limits.abort?.();
144
165
  await reader.cancel().catch(() => { });
166
+ const overTotal = Date.now() - startedAt >= totalMs;
145
167
  throw new ApiError({
146
168
  kind: "timeout",
147
169
  exitCode: ExitCode.Unavailable,
148
- message: `the stream sent nothing for ${idleMs}ms`,
170
+ message: overTotal
171
+ ? `the stream ran for longer than ${totalMs}ms`
172
+ : `the stream sent nothing for ${idleMs}ms`,
149
173
  });
150
174
  }
151
175
  const { done, value } = chunk;
@@ -162,8 +186,14 @@ export async function readChatStream(response, events = {}, signal, limits = {})
162
186
  });
163
187
  }
164
188
  for (const payload of framer.push(value)) {
165
- if (assembler.push(payload))
189
+ if (assembler.push(payload)) {
190
+ // R-3 · `[DONE]` means the answer is complete, not that the socket
191
+ // is. An upstream that keeps the connection open after it would
192
+ // otherwise hold ours too - releasing the reader lock in the
193
+ // `finally` does not close anything.
194
+ finished = true;
166
195
  return assembler.result();
196
+ }
167
197
  }
168
198
  }
169
199
  for (const payload of framer.end())
@@ -171,6 +201,12 @@ export async function readChatStream(response, events = {}, signal, limits = {})
171
201
  }
172
202
  finally {
173
203
  signal?.removeEventListener("abort", onAbort);
204
+ if (finished) {
205
+ // Cancel first, then release: cancelling closes the body, and the abort
206
+ // reaches the fetch itself, which is what actually frees the socket.
207
+ void reader.cancel().catch(() => { });
208
+ limits.abort?.();
209
+ }
174
210
  reader.releaseLock();
175
211
  }
176
212
  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,CAgInB;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";
@@ -13,17 +16,23 @@ import { WRITE_TOOLS } from "../agent/tools/write.js";
13
16
  import { Workspace, WorkspaceError } from "../filesystem/workspace.js";
14
17
  import { ExitCode } from "../output/exit-codes.js";
15
18
  import { renderJson, renderRecord } from "../output/format.js";
16
- import { formatUsd } from "../output/money.js";
19
+ import { formatUsdWithUnpriced } 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}`],
@@ -110,7 +127,10 @@ export async function agentCommand(argv, io) {
110
127
  "tokens",
111
128
  `${summary.promptTokens} in / ${summary.completionTokens} out`,
112
129
  ],
113
- ["cost", `~${formatUsd(summary.costMicroUsd)}`],
130
+ [
131
+ "cost",
132
+ `~${formatUsdWithUnpriced(summary.costMicroUsd, summary.unpricedRequests)}`,
133
+ ],
114
134
  ]));
115
135
  return ExitCode.Ok;
116
136
  }
@@ -149,6 +169,29 @@ export async function agentCommand(argv, io) {
149
169
  * writes nothing at all, and a store that cannot be opened costs the
150
170
  * transcript rather than the session.
151
171
  */
172
+ /**
173
+ * R-8 · The first-save notice, once per machine.
174
+ *
175
+ * The marker file is shared with the TUI on purpose: the notice is about this
176
+ * computer, not about which command happened to write first.
177
+ */
178
+ function showNoticeOnce(root, io) {
179
+ const marker = join(root, NOTICE_FILE);
180
+ try {
181
+ if (existsSync(marker))
182
+ return;
183
+ io.stderr(firstSaveNotice(root));
184
+ writeFileSync(marker, `${new Date().toISOString()}\n`, { mode: 0o600 });
185
+ }
186
+ catch {
187
+ // Telling the user is best effort; failing to record that we told them
188
+ // must not stop the session.
189
+ }
190
+ }
191
+ /** Whether a terminal is on the other end of stdout. Injectable for tests. */
192
+ function isTty(io) {
193
+ return io.isTty ?? process.stdout.isTTY === true;
194
+ }
152
195
  function openRecorder(parsed, io, modelId, workspaceRoot, mode) {
153
196
  const decision = decideRecording({
154
197
  surface: "interactive",
@@ -169,6 +212,10 @@ function openRecorder(parsed, io, modelId, workspaceRoot, mode) {
169
212
  workspace: workspaceRoot,
170
213
  mode,
171
214
  });
215
+ // R-8 · Said once per machine, on whichever surface records first. The
216
+ // TUI showed it and this one did not, so a `mafdet agent` user could have
217
+ // a transcript on disk without ever being told.
218
+ showNoticeOnce(root, io);
172
219
  return new SessionRecorder({
173
220
  writer,
174
221
  root,
@@ -300,11 +347,16 @@ function reportEvent(event, io) {
300
347
  /** Exit codes follow §4.5: a ceiling is 7, a cancel is 6. */
301
348
  function finish(report, json, io) {
302
349
  if (json) {
303
- io.stdout(renderJson({ ...report, costMicroUsd: report.costMicroUsd.toString() }));
350
+ io.stdout(
351
+ // T-4 · `unpricedRequests` rides along in the report, so a consumer can
352
+ // tell a genuinely free run from one nobody could price.
353
+ renderJson({ ...report, costMicroUsd: report.costMicroUsd.toString() }));
304
354
  }
305
355
  else {
306
- if (report.finalText)
307
- io.stdout(`${report.finalText}\n`);
356
+ // R-4 · The model's answer, sanitised only when a terminal is reading it.
357
+ if (report.finalText) {
358
+ io.stdout(`${safeForTerminal(report.finalText, isTty(io))}\n`);
359
+ }
308
360
  io.stderr("\n" +
309
361
  renderRecord([
310
362
  ["status", report.stopReason],
@@ -319,7 +371,10 @@ function finish(report, json, io) {
319
371
  "tokens",
320
372
  `${report.promptTokens} in / ${report.completionTokens} out`,
321
373
  ],
322
- ["cost", `~${formatUsd(report.costMicroUsd)}`],
374
+ [
375
+ "cost",
376
+ `~${formatUsdWithUnpriced(report.costMicroUsd, report.unpricedRequests)}`,
377
+ ],
323
378
  ]));
324
379
  if (report.error)
325
380
  io.stderr(`error: ${report.error}\n`);
@@ -334,6 +389,9 @@ function finish(report, json, io) {
334
389
  case "max_cost":
335
390
  // F-008 · A ceiling too: the window ran out, and the report says which.
336
391
  case "context_limit":
392
+ // R-5 · Asked for a ceiling we cannot enforce; stopped rather than spend
393
+ // past it. A limit, so it belongs with the other limits.
394
+ case "cost_unknown":
337
395
  // Stopped short on purpose; the report says so rather than pretending.
338
396
  return ExitCode.LimitReached;
339
397
  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"}
@@ -63,7 +63,13 @@ async function run(argv, io) {
63
63
  return ExitCode.Usage;
64
64
  }
65
65
  function list(argv, io, root) {
66
- const { format } = parseFormatFlags(argv);
66
+ const { format, rest } = parseFormatFlags(argv);
67
+ // R-7 · `list` was the one subcommand that threw its leftovers away, so
68
+ // `sessions list --dry-run` exited 0 and listed everything.
69
+ if (rest.length > 0) {
70
+ io.stderr(`mafdet sessions list: unexpected argument '${rest[0]}'\n${USAGE}`);
71
+ return ExitCode.Usage;
72
+ }
67
73
  const { sessions } = loadIndex(root);
68
74
  if (format === "json") {
69
75
  io.stdout(renderJson({
@@ -89,7 +95,12 @@ function list(argv, io, root) {
89
95
  { header: "REQUESTS", value: (s) => `${s.model_requests}` },
90
96
  {
91
97
  header: "EST. COST",
92
- value: (s) => formatUsd(s.estimated_charge_micro_usd),
98
+ // R-6 · An unpriced session is not a free one. `$0.00` in the main
99
+ // column is the claim that matters; a note on stderr cannot correct
100
+ // a number the user has already read.
101
+ value: (s) => s.unpriced_requests
102
+ ? "unknown"
103
+ : formatUsd(s.estimated_charge_micro_usd),
93
104
  },
94
105
  { header: "UPDATED", value: (s) => s.updated_at },
95
106
  ]));
@@ -135,12 +146,20 @@ function show(argv, io, root) {
135
146
  ["model", safe(m.model_id, io)],
136
147
  ["workspace", safe(m.workspace, io)],
137
148
  ["mode", safe(m.mode, io)],
138
- ["created", m.created_at],
139
- ["updated", m.updated_at],
149
+ // R-4 · Timestamps come off disk like everything else here; a file that
150
+ // has been edited can carry a sequence in any field, not only the
151
+ // free-text ones.
152
+ ["created", safe(m.created_at, io)],
153
+ ["updated", safe(m.updated_at, io)],
140
154
  ["messages", `${m.message_count}`],
141
155
  ["model requests", `${m.model_requests}`],
142
156
  ["tokens", `${m.prompt_tokens} in / ${m.completion_tokens} out`],
143
- ["estimated cost", formatUsd(m.estimated_charge_micro_usd)],
157
+ [
158
+ "estimated cost",
159
+ m.unpriced_requests
160
+ ? `${formatUsd(m.estimated_charge_micro_usd)} + ${m.unpriced_requests} unpriced`
161
+ : formatUsd(m.estimated_charge_micro_usd),
162
+ ],
144
163
  ]));
145
164
  if (session.truncatedTail) {
146
165
  // Worth saying plainly: the user should know a crash cost them the tail.
@@ -322,6 +341,8 @@ async function usage(argv, io, root) {
322
341
  completion_tokens: totals.completionTokens,
323
342
  // Strings, so no parser turns a charge into a float.
324
343
  estimated_charge_micro_usd: totals.estimatedMicroUsd.toString(),
344
+ // R-6 · Without this a consumer reads the total above as complete.
345
+ unpriced_requests: totals.unpricedRequests,
325
346
  settled_charge_micro_usd: totals.settledMicroUsd.toString(),
326
347
  by_settlement: totals.bySettlement,
327
348
  by_model: totals.byModel.map((m) => ({
@@ -330,11 +351,16 @@ async function usage(argv, io, root) {
330
351
  prompt_tokens: m.promptTokens,
331
352
  completion_tokens: m.completionTokens,
332
353
  estimated_charge_micro_usd: m.estimatedMicroUsd.toString(),
354
+ // T-10 · Without this a caller cannot tell whether one model's "0"
355
+ // is free or unpriced, even though the top-level total says some
356
+ // requests were not counted.
357
+ unpriced_requests: m.unpricedRequests,
333
358
  })),
334
359
  by_api_key: totals.byKey.map((k) => ({
335
360
  api_key_ref: k.apiKeyRef,
336
361
  model_requests: k.modelRequests,
337
362
  estimated_charge_micro_usd: k.estimatedMicroUsd.toString(),
363
+ unpriced_requests: k.unpricedRequests,
338
364
  })),
339
365
  ...(refreshed === undefined ? {} : { refresh: jsonRefresh(refreshed) }),
340
366
  }));
@@ -348,7 +374,12 @@ async function usage(argv, io, root) {
348
374
  `${totals.toolRuns}${totals.toolRunsRefused > 0 ? ` (${totals.toolRunsRefused} refused)` : ""}`,
349
375
  ],
350
376
  ["tokens", `${totals.promptTokens} in / ${totals.completionTokens} out`],
351
- ["local estimate", formatUsd(totals.estimatedMicroUsd.toString())],
377
+ [
378
+ "local estimate",
379
+ totals.unpricedRequests > 0
380
+ ? `${formatUsd(totals.estimatedMicroUsd.toString())} + ${totals.unpricedRequests} unpriced`
381
+ : formatUsd(totals.estimatedMicroUsd.toString()),
382
+ ],
352
383
  ];
353
384
  if (totals.bySettlement.settled > 0) {
354
385
  rows.push([
@@ -366,11 +397,13 @@ async function usage(argv, io, root) {
366
397
  if (totals.byModel.length > 1) {
367
398
  io.stdout("\nBy model\n");
368
399
  io.stdout(renderTable(totals.byModel, [
369
- { header: "MODEL", value: (m) => m.modelId },
400
+ { header: "MODEL", value: (m) => safe(m.modelId, io) },
370
401
  { header: "REQUESTS", value: (m) => `${m.modelRequests}` },
371
402
  {
372
403
  header: "ESTIMATE",
373
- value: (m) => formatUsd(m.estimatedMicroUsd.toString()),
404
+ value: (m) => m.unpricedRequests > 0
405
+ ? `${formatUsd(m.estimatedMicroUsd.toString())} + ${m.unpricedRequests} unpriced`
406
+ : formatUsd(m.estimatedMicroUsd.toString()),
374
407
  },
375
408
  ]));
376
409
  }
@@ -378,11 +411,13 @@ async function usage(argv, io, root) {
378
411
  // Split per key so two accounts' spending is never presented as one.
379
412
  io.stdout("\nBy API key\n");
380
413
  io.stdout(renderTable(totals.byKey, [
381
- { header: "KEY", value: (k) => k.apiKeyRef },
414
+ { header: "KEY", value: (k) => safe(k.apiKeyRef, io) },
382
415
  { header: "REQUESTS", value: (k) => `${k.modelRequests}` },
383
416
  {
384
417
  header: "ESTIMATE",
385
- value: (k) => formatUsd(k.estimatedMicroUsd.toString()),
418
+ value: (k) => k.unpricedRequests > 0
419
+ ? `${formatUsd(k.estimatedMicroUsd.toString())} + ${k.unpricedRequests} unpriced`
420
+ : formatUsd(k.estimatedMicroUsd.toString()),
386
421
  },
387
422
  ]));
388
423
  }
@@ -13,4 +13,13 @@ export declare function parseMicroUsd(value: string | number | bigint): bigint;
13
13
  export declare function formatUsd(microUsd: string | number | bigint, options?: {
14
14
  minFractionDigits?: number;
15
15
  }): string;
16
+ /**
17
+ * T-4 · A cost that may be incomplete, described rather than rounded to zero.
18
+ *
19
+ * Four surfaces render a running total - the command summary, the TUI summary,
20
+ * `/cost` and the JSON report - and each one showed `$0.00` for a run whose
21
+ * model the catalogue could not price. The session store had already learned
22
+ * to say "unknown"; the places the user actually watches had not.
23
+ */
24
+ export declare function formatUsdWithUnpriced(microUsd: bigint | string, unpricedRequests: number): string;
16
25
  //# sourceMappingURL=money.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../../src/output/money.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAOrE;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAClC,OAAO,GAAE;IAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3C,MAAM,CAeR"}
1
+ {"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../../src/output/money.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAOrE;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAClC,OAAO,GAAE;IAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3C,MAAM,CAeR;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,gBAAgB,EAAE,MAAM,GACvB,MAAM,CAIR"}
@@ -33,3 +33,17 @@ export function formatUsd(microUsd, options = {}) {
33
33
  digits = fraction.slice(0, minDigits);
34
34
  return `${negative ? "-" : ""}$${whole}.${digits}`;
35
35
  }
36
+ /**
37
+ * T-4 · A cost that may be incomplete, described rather than rounded to zero.
38
+ *
39
+ * Four surfaces render a running total - the command summary, the TUI summary,
40
+ * `/cost` and the JSON report - and each one showed `$0.00` for a run whose
41
+ * model the catalogue could not price. The session store had already learned
42
+ * to say "unknown"; the places the user actually watches had not.
43
+ */
44
+ export function formatUsdWithUnpriced(microUsd, unpricedRequests) {
45
+ const shown = formatUsd(typeof microUsd === "bigint" ? microUsd : microUsd);
46
+ if (unpricedRequests <= 0)
47
+ return shown;
48
+ return `${shown} + ${unpricedRequests} unpriced`;
49
+ }
@@ -7,6 +7,8 @@ export interface IndexEntry {
7
7
  readonly workspace: string;
8
8
  readonly model_requests: number;
9
9
  readonly estimated_charge_micro_usd: string;
10
+ /** R-6 · Requests the estimate above does not include; see F-012. */
11
+ readonly unpriced_requests?: number;
10
12
  }
11
13
  export interface SessionIndex {
12
14
  readonly schema_version: number;
@@ -1 +1 @@
1
- {"version":3,"file":"index-file.d.ts","sourceRoot":"","sources":["../../src/sessions/index-file.ts"],"names":[],"mappings":"AAqBA,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,YAAY,CAAC;AAKpB,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,0BAA0B,EAAE,MAAM,CAAC;CAC7C;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAC;CAC1C;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAmBD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAoBvD;AAED,oEAAoE;AACpE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,CAiBlE;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAapD;AAwBD,yEAAyE;AACzE,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,eAAe,GACxB,YAAY,CAUd;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,YAAY,CAed;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBnD"}
1
+ {"version":3,"file":"index-file.d.ts","sourceRoot":"","sources":["../../src/sessions/index-file.ts"],"names":[],"mappings":"AAqBA,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,YAAY,CAAC;AAKpB,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,0BAA0B,EAAE,MAAM,CAAC;IAC5C,qEAAqE;IACrE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAC;CAC1C;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAsBD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAoBvD;AAED,oEAAoE;AACpE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,CAiBlE;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAapD;AAwBD,yEAAyE;AACzE,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,eAAe,GACxB,YAAY,CAUd;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,YAAY,CAed;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBnD"}
@@ -25,6 +25,9 @@ function entryOf(metadata) {
25
25
  workspace: metadata.workspace,
26
26
  model_requests: metadata.model_requests,
27
27
  estimated_charge_micro_usd: metadata.estimated_charge_micro_usd,
28
+ ...(metadata.unpriced_requests
29
+ ? { unpriced_requests: metadata.unpriced_requests }
30
+ : {}),
28
31
  };
29
32
  }
30
33
  /** Newest first, which is the order a session list wants. */
@@ -1 +1 @@
1
- {"version":3,"file":"open.d.ts","sourceRoot":"","sources":["../../src/sessions/open.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAEL,oBAAoB,EAGpB,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAKrB,OAAO,EAAE,aAAa,EAAe,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAE9E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,sBAAsB,EAAE,SAAS,MAAM,EAAE,CAAC;CACpD;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACxD;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;CAC/D,CAAC;AAEN,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,UAAU,CAqFnE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,GAC9C,MAAM,CAwBR"}
1
+ {"version":3,"file":"open.d.ts","sourceRoot":"","sources":["../../src/sessions/open.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAEL,oBAAoB,EAGpB,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAKrB,OAAO,EAAE,aAAa,EAAe,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAE9E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,sBAAsB,EAAE,SAAS,MAAM,EAAE,CAAC;CACpD;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACxD;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;CAC/D,CAAC;AAEN,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,UAAU,CAqFnE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,GAC9C,MAAM,CAyBR"}
@@ -6,6 +6,7 @@
6
6
  // and the checks are all re-derived from the key in use *now*, never from
7
7
  // what the session was recorded with.
8
8
  import { existsSync } from "node:fs";
9
+ import { stripAnsi } from "../agent/command-policy.js";
9
10
  import { acquireSessionLock, SessionLockedError, } from "./lock.js";
10
11
  import { describeBlocker, permissionsForResume, rebuildContext, resumeBlockers, } from "./resume.js";
11
12
  import { InvalidSessionIdError, resolveSessionDirectory, } from "./session-id.js";
@@ -102,8 +103,9 @@ export function openForResume(input) {
102
103
  * know that will be surprised by the bill rather than by the feature.
103
104
  */
104
105
  export function describeResume(result) {
106
+ // R-4 · Title and model come off disk and this is drawn on a terminal.
105
107
  const lines = [
106
- `resuming "${result.metadata.title}" (${result.metadata.model_id})`,
108
+ `resuming "${stripAnsi(result.metadata.title)}" (${stripAnsi(result.metadata.model_id)})`,
107
109
  ];
108
110
  if (result.toolRunsSummarised > 0) {
109
111
  lines.push(` ${result.toolRunsSummarised} earlier tool run(s) are summarised, not repeated`);
@@ -50,6 +50,7 @@ export interface ResumeCheckInput {
50
50
  * with a key that could call a model says nothing about whether this key can.
51
51
  */
52
52
  export declare function resumeBlockers(input: ResumeCheckInput): readonly ResumeBlocker[];
53
+ /** R-4 · The values quoted here are recorded ones, and this is shown on a terminal. */
53
54
  export declare function describeBlocker(blocker: ResumeBlocker): string;
54
55
  /**
55
56
  * What a resumed run starts with, stated so the caller cannot forget.
@@ -1 +1 @@
1
- {"version":3,"file":"resume.d.ts","sourceRoot":"","sources":["../../src/sessions/resume.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACjC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,wEAAwE;IACxE,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACrC;AAID;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,cAAc,CAwClE;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAChE;IAAE,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACtE;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,8CAA8C;IAC9C,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,gBAAgB,GACtB,SAAS,aAAa,EAAE,CAa1B;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAS9D;AAED;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,QAAQ,CAAC,cAAc,EAAE,KAAK,CAAC;CAChC;AAED,wBAAgB,oBAAoB,CAClC,eAAe,EAAE,SAAS,MAAM,EAAE,GACjC,kBAAkB,CAQpB"}
1
+ {"version":3,"file":"resume.d.ts","sourceRoot":"","sources":["../../src/sessions/resume.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACjC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,wEAAwE;IACxE,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACrC;AAID;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,cAAc,CAwClE;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAChE;IAAE,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACtE;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,8CAA8C;IAC9C,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,gBAAgB,GACtB,SAAS,aAAa,EAAE,CAa1B;AAED,uFAAuF;AACvF,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAS9D;AAED;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,QAAQ,CAAC,cAAc,EAAE,KAAK,CAAC;CAChC;AAED,wBAAgB,oBAAoB,CAClC,eAAe,EAAE,SAAS,MAAM,EAAE,GACjC,kBAAkB,CAQpB"}
@@ -9,6 +9,7 @@
9
9
  // what was said. A replayed `tool_calls` block would invite the model to
10
10
  // continue a plan the user approved yesterday, in a workspace that has since
11
11
  // changed, with permissions this run may not have.
12
+ import { stripAnsi } from "../agent/command-policy.js";
12
13
  const DEFAULT_MAX_MESSAGES = 40;
13
14
  /**
14
15
  * Rebuild the messages to send, from what was stored.
@@ -79,14 +80,15 @@ export function resumeBlockers(input) {
79
80
  }
80
81
  return blockers;
81
82
  }
83
+ /** R-4 · The values quoted here are recorded ones, and this is shown on a terminal. */
82
84
  export function describeBlocker(blocker) {
83
85
  switch (blocker.kind) {
84
86
  case "model-unavailable":
85
- return `${blocker.modelId} is not callable with the key you are using now - choose another model to continue`;
87
+ return `${stripAnsi(blocker.modelId)} is not callable with the key you are using now - choose another model to continue`;
86
88
  case "model-not-agent-capable":
87
- return `${blocker.modelId} has no verified tool calling now, so the agent cannot drive it - choose another model`;
89
+ return `${stripAnsi(blocker.modelId)} has no verified tool calling now, so the agent cannot drive it - choose another model`;
88
90
  case "workspace-missing":
89
- return `${blocker.workspace} no longer exists - you can read this conversation, or continue it in another directory`;
91
+ return `${stripAnsi(blocker.workspace)} no longer exists - you can read this conversation, or continue it in another directory`;
90
92
  }
91
93
  }
92
94
  export function permissionsForResume(declaredThisRun) {
@@ -1 +1 @@
1
- {"version":3,"file":"agent-surface.d.ts","sourceRoot":"","sources":["../../src/tui/agent-surface.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,UAAU,EACV,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAClD,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,IAAI,CACpB,cAAc,EACd,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,CAC1D,CAAC;IACF;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,WAAW,GAAG,SAAS,CAAC;IACnD,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;IACpC,yDAAyD;IACzD,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,UAAU,CAAC;CAClC;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA+B7B;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,cAAc,CAAC,CA4EzB;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,cAAc,EACvB,YAAY,EAAE,oBAAoB,GACjC,MAAM,CAgBR"}
1
+ {"version":3,"file":"agent-surface.d.ts","sourceRoot":"","sources":["../../src/tui/agent-surface.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,UAAU,EACV,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAClD,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,IAAI,CACpB,cAAc,EACd,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,CAC1D,CAAC;IACF;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,WAAW,GAAG,SAAS,CAAC;IACnD,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;IACpC,yDAAyD;IACzD,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,UAAU,CAAC;CAClC;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA+B7B;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,cAAc,CAAC,CA+EzB;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,cAAc,EACvB,YAAY,EAAE,oBAAoB,GACjC,MAAM,CAqBR"}