@intentic/sandbox-contract 1.308.2 → 1.309.0

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 (168) hide show
  1. package/dist/contracts/agent.contract.d.ts +98 -4
  2. package/dist/contracts/agent.contract.d.ts.map +1 -1
  3. package/dist/contracts/agent.contract.js +40 -10
  4. package/dist/contracts/agent.contract.js.map +1 -1
  5. package/dist/contracts/agents.contract.d.ts +285 -0
  6. package/dist/contracts/agents.contract.d.ts.map +1 -1
  7. package/dist/contracts/approvals.contract.d.ts +34 -0
  8. package/dist/contracts/approvals.contract.d.ts.map +1 -1
  9. package/dist/contracts/approvals.contract.js +30 -1
  10. package/dist/contracts/approvals.contract.js.map +1 -1
  11. package/dist/contracts/device.contract.d.ts +1 -0
  12. package/dist/contracts/device.contract.d.ts.map +1 -1
  13. package/dist/contracts/extensions.contract.d.ts +390 -0
  14. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  15. package/dist/contracts/extensions.contract.js +11 -1
  16. package/dist/contracts/extensions.contract.js.map +1 -1
  17. package/dist/contracts/runner.contract.d.ts +117 -107
  18. package/dist/contracts/runner.contract.d.ts.map +1 -1
  19. package/dist/contracts/sessions.contract.d.ts +1 -0
  20. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  21. package/dist/contracts/settings.contract.d.ts +54 -0
  22. package/dist/contracts/settings.contract.d.ts.map +1 -1
  23. package/dist/contracts/system.contract.d.ts +210 -0
  24. package/dist/contracts/system.contract.d.ts.map +1 -1
  25. package/dist/contracts/system.contract.js +35 -1
  26. package/dist/contracts/system.contract.js.map +1 -1
  27. package/dist/events/agent-events.d.ts +9 -2
  28. package/dist/events/agent-events.d.ts.map +1 -1
  29. package/dist/events/agent-events.js +5 -1
  30. package/dist/events/agent-events.js.map +1 -1
  31. package/dist/events/resume.d.ts +1 -0
  32. package/dist/events/resume.d.ts.map +1 -1
  33. package/dist/events/resume.js +2 -0
  34. package/dist/events/resume.js.map +1 -1
  35. package/dist/events/system-events.d.ts +40 -0
  36. package/dist/events/system-events.d.ts.map +1 -1
  37. package/dist/events/transcript.d.ts +14 -0
  38. package/dist/events/transcript.d.ts.map +1 -1
  39. package/dist/events/transcript.js +7 -1
  40. package/dist/events/transcript.js.map +1 -1
  41. package/dist/index.d.ts +1141 -71
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +2 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/models/model-label.d.ts +2 -0
  46. package/dist/models/model-label.d.ts.map +1 -0
  47. package/dist/models/model-label.js +20 -0
  48. package/dist/models/model-label.js.map +1 -0
  49. package/dist/models/model-pins.d.ts +0 -1
  50. package/dist/models/model-pins.d.ts.map +1 -1
  51. package/dist/models/model-pins.js +0 -2
  52. package/dist/models/model-pins.js.map +1 -1
  53. package/dist/policy/command-classes.d.ts.map +1 -1
  54. package/dist/policy/command-classes.js +88 -7
  55. package/dist/policy/command-classes.js.map +1 -1
  56. package/dist/policy/reserved-servers.d.ts +7 -0
  57. package/dist/policy/reserved-servers.d.ts.map +1 -0
  58. package/dist/policy/reserved-servers.js +21 -0
  59. package/dist/policy/reserved-servers.js.map +1 -0
  60. package/dist/policy/turned-away.d.ts.map +1 -1
  61. package/dist/policy/turned-away.js.map +1 -1
  62. package/dist/protocol/ingress-protocol.d.ts +2 -0
  63. package/dist/protocol/ingress-protocol.d.ts.map +1 -1
  64. package/dist/protocol/ingress-protocol.js +4 -1
  65. package/dist/protocol/ingress-protocol.js.map +1 -1
  66. package/dist/protocol/peer-mcp-server.d.ts +7 -0
  67. package/dist/protocol/peer-mcp-server.d.ts.map +1 -1
  68. package/dist/protocol/peer-mcp-server.js +6 -1
  69. package/dist/protocol/peer-mcp-server.js.map +1 -1
  70. package/dist/protocol/tunnel-heartbeat.d.ts +19 -0
  71. package/dist/protocol/tunnel-heartbeat.d.ts.map +1 -0
  72. package/dist/protocol/tunnel-heartbeat.js +41 -0
  73. package/dist/protocol/tunnel-heartbeat.js.map +1 -0
  74. package/dist/schemas/agent.d.ts +59 -0
  75. package/dist/schemas/agent.d.ts.map +1 -1
  76. package/dist/schemas/agent.js +45 -2
  77. package/dist/schemas/agent.js.map +1 -1
  78. package/dist/schemas/agents.d.ts +60 -0
  79. package/dist/schemas/agents.d.ts.map +1 -1
  80. package/dist/schemas/agents.js +6 -1
  81. package/dist/schemas/agents.js.map +1 -1
  82. package/dist/schemas/approvals.d.ts +67 -0
  83. package/dist/schemas/approvals.d.ts.map +1 -1
  84. package/dist/schemas/approvals.js +43 -0
  85. package/dist/schemas/approvals.js.map +1 -1
  86. package/dist/schemas/automations.d.ts +20 -0
  87. package/dist/schemas/automations.d.ts.map +1 -1
  88. package/dist/schemas/devices.d.ts +2 -0
  89. package/dist/schemas/devices.d.ts.map +1 -1
  90. package/dist/schemas/devices.js +1 -0
  91. package/dist/schemas/devices.js.map +1 -1
  92. package/dist/schemas/extension-updates.d.ts +850 -77
  93. package/dist/schemas/extension-updates.d.ts.map +1 -1
  94. package/dist/schemas/extension-updates.js +23 -0
  95. package/dist/schemas/extension-updates.js.map +1 -1
  96. package/dist/schemas/history.d.ts +1 -0
  97. package/dist/schemas/history.d.ts.map +1 -1
  98. package/dist/schemas/history.js +4 -0
  99. package/dist/schemas/history.js.map +1 -1
  100. package/dist/schemas/metrics.d.ts +258 -0
  101. package/dist/schemas/metrics.d.ts.map +1 -1
  102. package/dist/schemas/metrics.js +95 -0
  103. package/dist/schemas/metrics.js.map +1 -1
  104. package/dist/schemas/providers/plan-limits.d.ts +42 -1
  105. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  106. package/dist/schemas/providers/plan-limits.js +48 -1
  107. package/dist/schemas/providers/plan-limits.js.map +1 -1
  108. package/dist/schemas/providers/usage.d.ts +2 -0
  109. package/dist/schemas/providers/usage.d.ts.map +1 -1
  110. package/dist/schemas/providers/usage.js +2 -0
  111. package/dist/schemas/providers/usage.js.map +1 -1
  112. package/dist/schemas/settings.d.ts +52 -0
  113. package/dist/schemas/settings.d.ts.map +1 -1
  114. package/dist/schemas/settings.js +11 -0
  115. package/dist/schemas/settings.js.map +1 -1
  116. package/dist/schemas/turn-break.d.ts +5 -0
  117. package/dist/schemas/turn-break.d.ts.map +1 -1
  118. package/dist/schemas/turn-break.js +4 -0
  119. package/dist/schemas/turn-break.js.map +1 -1
  120. package/dist/state/definition.d.ts +8 -0
  121. package/dist/state/definition.d.ts.map +1 -1
  122. package/dist/state/history-state.d.ts.map +1 -1
  123. package/dist/state/history-state.js +3 -0
  124. package/dist/state/history-state.js.map +1 -1
  125. package/dist/text/transcript-fold.d.ts +1 -1
  126. package/dist/text/transcript-fold.d.ts.map +1 -1
  127. package/dist/text/transcript-fold.js +19 -9
  128. package/dist/text/transcript-fold.js.map +1 -1
  129. package/package.json +16 -5
  130. package/src/contracts/agent.contract.ts +62 -15
  131. package/src/contracts/approvals.contract.ts +35 -1
  132. package/src/contracts/extensions.contract.ts +14 -0
  133. package/src/contracts/system.contract.ts +42 -1
  134. package/src/events/agent-events.ts +10 -2
  135. package/src/events/resume.test.ts +8 -0
  136. package/src/events/resume.ts +4 -0
  137. package/src/events/transcript.ts +13 -2
  138. package/src/index.ts +2 -0
  139. package/src/models/model-label.test.ts +24 -0
  140. package/src/models/model-label.ts +29 -0
  141. package/src/models/model-pins.ts +0 -6
  142. package/src/policy/command-classes.test.ts +99 -1
  143. package/src/policy/command-classes.ts +143 -11
  144. package/src/policy/reserved-servers.test.ts +51 -0
  145. package/src/policy/reserved-servers.ts +48 -0
  146. package/src/policy/turned-away.ts +2 -1
  147. package/src/protocol/ingress-protocol.test.ts +78 -1
  148. package/src/protocol/ingress-protocol.ts +8 -1
  149. package/src/protocol/peer-mcp-server.test.ts +10 -3
  150. package/src/protocol/peer-mcp-server.ts +14 -1
  151. package/src/protocol/tunnel-heartbeat.test.ts +76 -0
  152. package/src/protocol/tunnel-heartbeat.ts +67 -0
  153. package/src/schemas/agent.ts +77 -5
  154. package/src/schemas/agents.ts +11 -1
  155. package/src/schemas/approvals.ts +67 -0
  156. package/src/schemas/context-trim.test.ts +2 -2
  157. package/src/schemas/devices.ts +2 -0
  158. package/src/schemas/extension-updates.ts +30 -0
  159. package/src/schemas/history.ts +8 -1
  160. package/src/schemas/metrics.ts +148 -0
  161. package/src/schemas/providers/plan-limits.ts +67 -2
  162. package/src/schemas/providers/usage.ts +3 -0
  163. package/src/schemas/settings.ts +16 -0
  164. package/src/schemas/turn-break.ts +8 -0
  165. package/src/state/history-state.ts +6 -0
  166. package/src/state/workspace-state.test.ts +2 -2
  167. package/src/text/transcript-fold.test.ts +25 -7
  168. package/src/text/transcript-fold.ts +32 -19
@@ -214,6 +214,14 @@ export const SteerSchema = z
214
214
  .object({
215
215
  conversationId: z.string().min(1).describe("Which running conversation to interrupt."),
216
216
  text: z.string().max(20_000).describe("What to say to it. It arrives mid-turn without stopping the turn."),
217
+ messageId: z
218
+ .string()
219
+ .min(1)
220
+ .max(128)
221
+ .optional()
222
+ .describe(
223
+ "Your id for this message. Sending again under an id the sandbox already took is answered with what it did with it the first time, never a second delivery. Leave it out and the sandbox names the message itself.",
224
+ ),
217
225
  attachments: z
218
226
  .array(z.string().min(1))
219
227
  .max(20)
@@ -233,8 +241,42 @@ export const SteerSchema = z
233
241
  .refine((steer) => steer.text.trim().length > 0 || (steer.attachments?.length ?? 0) > 0, {
234
242
  message: "text or attachments required",
235
243
  });
236
- // True cancel, aborted daemon-side; unlike closing the /agent fetch, which sends no cancel frame.
237
- export const StopTurnSchema = z.object({ conversationId: z.string().min(1).describe("Which conversation's running turn to cancel.") });
244
+ // True cancel, aborted daemon-side; unlike closing the /agent fetch, which sends no cancel frame. A stop names the run
245
+ // it means, so one arriving after that run settled cannot cancel whatever the conversation started next.
246
+ export const StopTurnSchema = z.union([
247
+ z.object({
248
+ conversationId: z.string().min(1).describe("Which conversation's running turn to cancel."),
249
+ run: z
250
+ .string()
251
+ .min(1)
252
+ .describe(
253
+ "The run you mean to cancel, as starting or attaching to it named it. If another turn has started since, nothing is cancelled and the answer names the one running instead.",
254
+ ),
255
+ }),
256
+ // A send not yet answered has no run to name, only the id it gave its message.
257
+ z.object({
258
+ conversationId: z.string().min(1).describe("Which conversation's running turn to cancel."),
259
+ messageId: z
260
+ .string()
261
+ .min(1)
262
+ .describe(
263
+ "The message you sent, while its run is not named yet: cancels the turn it is in. If none is, nothing is cancelled: the message has not become a turn, or its turn has already ended.",
264
+ ),
265
+ }),
266
+ // Only for a turn nobody can name: one the sandbox runs with no run to attach to (a scheduled automation's).
267
+ z.object({
268
+ conversationId: z.string().min(1).describe("Which conversation's running turn to cancel."),
269
+ live: z
270
+ .literal(true)
271
+ .describe("Cancel whatever turn is running now, whichever that is. Only for a turn you cannot name: one that has no run to attach to."),
272
+ }),
273
+ ]);
274
+ export type StopTurn = z.infer<typeof StopTurnSchema>;
275
+ export const StopResultSchema = z.object({
276
+ stopped: z.boolean().describe("Whether a turn was cancelled."),
277
+ running: z.string().optional().describe("The run that is live instead of the one you named, left running. Absent when nothing else runs."),
278
+ });
279
+ export type StopResult = z.infer<typeof StopResultSchema>;
238
280
  // The press carries WHO serves the re-run; the daemon keeps WHAT the turn is (prompt, attachments, worktree) from its
239
281
  // own record, since re-deriving from a stripped client transcript would replay a different turn.
240
282
  export const ResumeRoutingSchema = z.object({
@@ -261,3 +303,26 @@ export const ResumeTurnSchema = z.object({
261
303
  "Who serves the re-run, when the conversation has been re-pointed since it was refused. Leave it out to run it on whatever the turn carried.",
262
304
  ),
263
305
  });
306
+ // Names one queued message as it was read, so a change made against an older copy is refused rather than written over
307
+ // somebody else's.
308
+ export const QueuedMessageRefSchema = z.object({
309
+ conversationId: z.string().min(1).describe("Whose queue."),
310
+ id: z.string().min(1).describe("Which waiting message."),
311
+ revision: z
312
+ .number()
313
+ .int()
314
+ .nonnegative()
315
+ .describe("The message's revision as you read it. If it has been changed since, from this window or another, nothing happens."),
316
+ });
317
+ export type QueuedMessageRef = z.infer<typeof QueuedMessageRefSchema>;
318
+ export const QueueEditSchema = QueuedMessageRefSchema.extend({
319
+ text: z.string().describe("What the message should say instead. It keeps its place in the queue."),
320
+ });
321
+ export type QueueEdit = z.infer<typeof QueueEditSchema>;
322
+ export const QueueResumeSchema = z.object({
323
+ conversationId: z.string().min(1).describe("Whose queue to let go."),
324
+ routing: ResumeRoutingSchema.optional().describe(
325
+ "Who serves the turn the waiting messages start, when the conversation has been re-pointed since they were queued: the usual answer to a refusal that held them. Leave it out to send them as they were queued.",
326
+ ),
327
+ });
328
+ export type QueueResume = z.infer<typeof QueueResumeSchema>;
@@ -72,6 +72,9 @@ export const UsageTurnSchema = z.object({
72
72
  // Hash of the brief that was sent, recorded on control turns too. Load-bearing rather than decorative: the file is
73
73
  // rewritten monthly, so a 30-day window holds two different treatments and pooling them would measure neither.
74
74
  notesCohort: z.string().optional(),
75
+ // Arm of the lean-guidance experiment, stable per conversation; true is the short form. The cohort hashes both forms.
76
+ guidanceArm: z.boolean().optional(),
77
+ guidanceCohort: z.string().optional(),
75
78
  // What became of pre-turn retrieval on this turn, and how long it took. Assignment and DELIVERY are different
76
79
  // facts: the first version of this mechanism was assigned to every eligible turn and reached four in five of them,
77
80
  // which is the difference between a null result and a mechanism that never ran. Absent means the flag was off, so
@@ -254,6 +254,21 @@ export const SandboxSettingsSchema = z.object({
254
254
  .describe(
255
255
  "Your own instructions, used only when the mode above says custom. Then it is the whole of them: both built-in bases go, and so does everything this product would otherwise add, including the guidance the chat's own cards are driven by. That is the price of total control.",
256
256
  ),
257
+ // Measured by the `guidance` experiment against the full set; the full set stays the default until that says otherwise.
258
+ leanGuidance: z
259
+ .boolean()
260
+ .default(false)
261
+ .describe(
262
+ "Send this product's own guidance in its short form: only what the agent cannot find out by looking, instead of a paragraph for every habit it was once caught in. Off by default, because the long form is the one the product was tuned on.",
263
+ ),
264
+ leanGuidanceHoldout: z
265
+ .number()
266
+ .min(0)
267
+ .max(1)
268
+ .default(0)
269
+ .describe(
270
+ "What share of conversations to keep on the long form, so the two can be compared. Whole conversations rather than individual turns, because the guidance sits in the prompt for the whole session.",
271
+ ),
257
272
  iqSearch: z
258
273
  .boolean()
259
274
  .default(false)
@@ -578,6 +593,7 @@ export const SavingsReportSchema = z.object({
578
593
  // Same absence rule as `search`: not measured, never zero.
579
594
  map: TurnExperimentSchema.optional(),
580
595
  notes: TurnExperimentSchema.optional(),
596
+ guidance: TurnExperimentSchema.optional(),
581
597
  });
582
598
  export type SavingsReport = z.infer<typeof SavingsReportSchema>;
583
599
 
@@ -47,3 +47,11 @@ export const RETRY_LADDER_TRIES = RETRY_LADDER_MS.length;
47
47
 
48
48
  /** The wait before the next rung, or undefined once the ladder is spent. */
49
49
  export const retryLadderDelay = (triesWithoutProgress: number): number | undefined => RETRY_LADDER_MS[triesWithoutProgress];
50
+
51
+ // Where an automatic re-run ladder stands, whichever wall climbs it (an outage's breaker, a stopped turn's rungs).
52
+ // `made` equal to `max` is a spent ladder: nothing more goes unasked.
53
+ export const RetryLadderSchema = z.object({
54
+ made: z.number().int().min(0).describe("Automatic re-runs already sent for this turn."),
55
+ max: z.number().int().min(1).describe("How many the ladder may send before it stands down."),
56
+ });
57
+ export type RetryLadder = z.infer<typeof RetryLadderSchema>;
@@ -113,5 +113,11 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
113
113
  { path: "runner-pair-consumed.json", portability: "identity" },
114
114
  // This container's own identity and reconnect token; elsewhere it would dial the parent as this runner.
115
115
  { path: "runner-identity.json", portability: "identity" },
116
+ // The owner's yes to a workspace's hooks, pinned to their bytes: a trust decision the target's owner makes again.
117
+ { path: "hook-approvals.json", portability: "identity", note: "Approve the workspace's Claude Code hooks again from Approvals." },
118
+ // The hook sets waiting for that yes; the next turn that finds them lists them again.
119
+ { path: "hook-requests.json", portability: "derived" },
120
+ // The owner's yes to each workspace extension's declared powers, decided again by the target's owner.
121
+ { path: "extension-approvals.json", portability: "identity", note: "Approve the extensions written in this workspace again from Extensions." },
116
122
  { path: "local-cert/", portability: "identity" },
117
123
  ];
@@ -60,8 +60,8 @@ describe(`staleQueryKeys`, () => {
60
60
  });
61
61
 
62
62
  it(`ignores a store's own temp file while it is mid-swap`, () => {
63
- // jsonFile's atomic-rename temp name: `.<name>.<pid>.tmp` beside the target.
64
- expect(staleQueryKeys([`.intentic/.settings.json.42.tmp`], [])).toEqual([]);
63
+ // writeTextFile's atomic-rename temp name: `.<name>.<pid>.<n>.tmp` beside the target.
64
+ expect(staleQueryKeys([`.intentic/.settings.json.42.7.tmp`], [])).toEqual([]);
65
65
  expect(staleQueryKeys([`.intentic/config/settings.json`], [])).toEqual([`settings`, `manifests`]);
66
66
  });
67
67
 
@@ -186,7 +186,8 @@ describe("foldTurn", () => {
186
186
  code: "provider-outage",
187
187
  message: "Anthropic is down.",
188
188
  autoResume: "scheduled",
189
- outage: { retryAt: 1, attempt: 2, maxAttempts: 6 },
189
+ outage: { retryAt: 1 },
190
+ retries: { made: 1, max: 6 },
190
191
  },
191
192
  ];
192
193
  // The row states what happened and nothing more: what happens next, and the one control that changes it, are
@@ -200,6 +201,23 @@ describe("foldTurn", () => {
200
201
  });
201
202
  });
202
203
 
204
+ // The count lives in the record, so a reader scrolling back sees how many times a stuck turn was sent again.
205
+ it("says on a stopped turn's row which automatic try comes next, or that the ladder stood down", () => {
206
+ const timedOut = "Google turn timed out waiting for OpenCode.";
207
+ const booked: AgentEvent[] = [{ kind: "error", message: timedOut, autoResume: "scheduled", nextAt: 1, retries: { made: 1, max: 3 } }];
208
+ expect(foldOf("hi", booked).at(-1)).toEqual({ role: "notice", text: `${timedOut} Retrying by itself: attempt 2 of 3.` });
209
+ const spent: AgentEvent[] = [{ kind: "error", message: timedOut, autoResume: "available", retries: { made: 3, max: 3 } }];
210
+ expect(foldOf("hi", spent).at(-1)).toEqual({
211
+ role: "notice",
212
+ text: `${timedOut} Retried 3 of 3 times by itself; nothing more is sent automatically.`,
213
+ });
214
+ // An outage nobody armed says so, instead of promising the breaker's retry.
215
+ const unarmed: AgentEvent[] = [
216
+ { kind: "error", code: "provider-outage", message: "Anthropic is down.", autoResume: "available", outage: { retryAt: 1 }, retries: { made: 0, max: 6 } },
217
+ ];
218
+ expect(foldOf("hi", unarmed).at(-1)).toEqual({ role: "notice", text: "Anthropic is down. Nothing is retrying it, so the turn is waiting here." });
219
+ });
220
+
203
221
  it("writes the turn's own events down as notices", () => {
204
222
  const events: AgentEvent[] = [
205
223
  { kind: "worktree", branch: "agent/x", base: "abc1234", sync: { commits: 2, blocked: [] } },
@@ -213,7 +231,7 @@ describe("foldTurn", () => {
213
231
  { role: "notice", text: "Context compacted to free up space." },
214
232
  {
215
233
  role: "notice",
216
- text: "Changes landed in your workspace: review them in the Changes panel. Installing 1 new dependency it added; the project's checks run when that finishes, and the outcome lands in Activity.",
234
+ text: "Changes landed in your workspace: review them in the Changes panel. Installing 1 dependency it added or changed; the project's checks run when that finishes, and the outcome lands in Activity.",
217
235
  noticeAction: "depsInstall",
218
236
  },
219
237
  ]);
@@ -495,8 +513,8 @@ describe("patches", () => {
495
513
  });
496
514
  });
497
515
 
498
- // A refusal that ran nothing offers the message back to the composer — true where somebody typed one, false for a run
499
- // that started itself, where there is no composer and no typed message.
516
+ // A refusal that ran nothing offers the message back for another press — true where somebody typed one, false for a
517
+ // run that started itself, where there is no composer and no typed message.
500
518
  describe(`a refusal that ran nothing`, () => {
501
519
  const REFUSED = { kind: `error`, code: `context-window-too-small`, message: `This model accepts 16,384 tokens.` } as const satisfies AgentEvent;
502
520
 
@@ -551,9 +569,9 @@ describe(`a refusal that ran nothing`, () => {
551
569
  expect(foldOf(`land it`, [REFUSED]).at(-1)?.noticeAction).toBeUndefined();
552
570
  });
553
571
 
554
- // The bug this retraction exists for: the composer keeps the words for another press, so a bubble left standing
555
- // here is the SAME message a second time, and a third, once per press against a gate that keeps refusing.
556
- it(`takes the message back out, leaving the composer's copy as the only one`, () => {
572
+ // The bug this retraction exists for: the conversation's queue keeps the words for another press, so a bubble left
573
+ // standing here is the SAME message a second time, and a third, once per press against a gate that keeps refusing.
574
+ it(`takes the message back out, leaving the queue's copy as the only one`, () => {
557
575
  const fold = new TranscriptFold(openingOf(`land it`));
558
576
  const patches = fold.apply(REFUSED);
559
577
 
@@ -62,10 +62,11 @@ const dependencyLine = (deps: { missing: number; started: string[]; deferred: bo
62
62
  if (deps === undefined || deps.missing === 0) {
63
63
  return ``;
64
64
  }
65
- const what = `${deps.missing} new ${deps.missing === 1 ? `dependency` : `dependencies`}`;
65
+ // `missing` counts versions the turn bumped as well as names it added, so the sentence names both.
66
+ const what = `${deps.missing} ${deps.missing === 1 ? `dependency` : `dependencies`} it added or changed`;
66
67
  return deps.deferred
67
- ? ` ${what} are queued: installation starts after this turn and any other active agents finish, appears in Work terminals, then its checks and outcome land in Activity.`
68
- : ` Installing ${what} it added; the project's checks run when that finishes, and the outcome lands in Activity.`;
68
+ ? ` ${what} ${deps.missing === 1 ? `is` : `are`} queued: installation starts after this turn and any other active agents finish, appears in Work terminals, then its checks and outcome land in Activity.`
69
+ : ` Installing ${what}; the project's checks run when that finishes, and the outcome lands in Activity.`;
69
70
  };
70
71
 
71
72
  // One line reporting the daemon's rebase of this agent's branch onto the current workspace; `blocked` names repos it
@@ -106,10 +107,10 @@ const landedRow = (event: Extract<AgentEvent, { kind: "landed" }>): TranscriptRo
106
107
  };
107
108
  };
108
109
 
109
- // The clause after a refusal that ran nothing. A turn somebody typed is held in the composer for another press; a turn
110
- // that started itself — an automation, a loop, a watch wake — has no typed message and nobody watching, so promising a
111
- // resend there names a composer the reader is not looking at. Neither version promises a retry: whether one comes is
112
- // the scheduler's decision, not this row's to state.
110
+ // The clause after a refusal that ran nothing. A turn somebody typed is held in its conversation's queue for another
111
+ // press; a turn that started itself — an automation, a loop, a watch wake — has no typed message and nobody watching,
112
+ // so promising a resend there names a composer the reader is not looking at. Neither version promises a retry: whether
113
+ // one comes is the scheduler's decision, not this row's to state.
113
114
  const undelivered = (unattended: boolean): string =>
114
115
  unattended
115
116
  ? `Nothing ran, and nothing is held: this run started on its own, so there is no message waiting to be sent again.`
@@ -152,9 +153,7 @@ const errorRow = (event: Extract<AgentEvent, { kind: "error" }>): TranscriptRow
152
153
  const { message, code } = event;
153
154
  switch (code) {
154
155
  case "provider-outage":
155
- return event.outage === undefined
156
- ? { role: "notice", text: `${message} Nothing is retrying it, so the turn is waiting here.` }
157
- : { role: "notice", text: `${message} Retrying by itself: attempt ${event.outage.attempt} of ${event.outage.maxAttempts}.` };
156
+ return { role: "notice", text: `${message} ${retryClause(event) ?? `Nothing is retrying it, so the turn is waiting here.`}` };
158
157
  case "claude-token-refused":
159
158
  return event.autoResume === "scheduled"
160
159
  ? {
@@ -169,18 +168,31 @@ const errorRow = (event: Extract<AgentEvent, { kind: "error" }>): TranscriptRow
169
168
  break;
170
169
  }
171
170
  if (!turnedAwayCode(code)) {
172
- return { role: "notice", text: message };
171
+ const clause = retryClause(event);
172
+ return { role: "notice", text: clause === undefined ? message : `${message} ${clause}` };
173
173
  }
174
174
  return heldRow(event);
175
175
  };
176
176
 
177
- // The turn's opening user row: text, timestamp, attachments, and whatever the daemon later stamps onto it (checkpoint,
178
- // notes).
179
- export const userRow = (text: string, sentAt: number, attachments: readonly string[]): TranscriptRow => {
177
+ // What a laddered wall's automatic re-runs are doing, said on the failure's own row so the count stays in the record.
178
+ const retryClause = (event: Extract<AgentEvent, { kind: "error" }>): string | undefined => {
179
+ const { retries } = event;
180
+ if (retries === undefined) {
181
+ return undefined;
182
+ }
183
+ if (event.autoResume === "scheduled") {
184
+ return `Retrying by itself: attempt ${retries.made + 1} of ${retries.max}.`;
185
+ }
186
+ return retries.made >= retries.max ? `Retried ${retries.made} of ${retries.max} times by itself; nothing more is sent automatically.` : undefined;
187
+ };
188
+
189
+ // The turn's opening user row: text, timestamp, attachments, the message's id, and whatever the daemon later stamps onto
190
+ // it (checkpoint, notes).
191
+ export const userRow = (text: string, sentAt: number, attachments: readonly string[], messageId?: string): TranscriptRow => {
180
192
  // Drops an attachment already inline as an @-mention, unless it's a generated upload path (its own thumbnail).
181
193
  const inline = new Set(mentionedPathTokens(text));
182
194
  const chips = attachments.filter((path) => !inline.has(path) || path.includes(`/records/artifacts/attachments/`));
183
- return { role: "user", text, sentAt, ...(chips.length > 0 ? { attachments: chips } : {}) };
195
+ return { role: "user", text, sentAt, ...(chips.length > 0 ? { attachments: chips } : {}), ...(messageId === undefined ? {} : { messageId }) };
184
196
  };
185
197
 
186
198
  export class TranscriptFold {
@@ -196,7 +208,7 @@ export class TranscriptFold {
196
208
  private readonly parked = new Map<string, number>();
197
209
  // The turn's opening user row, where the checkpoint and daemon notes land; cleared once `retract` takes it back.
198
210
  private opener: number | undefined;
199
- // Set by `retract`: this turn was refused before it ran, and its message went back to the composer.
211
+ // Set by `retract`: this turn was refused before it ran, and its message went back to the conversation's queue.
200
212
  private unrun = false;
201
213
 
202
214
  constructor(
@@ -426,6 +438,7 @@ export class TranscriptFold {
426
438
  text: event.text,
427
439
  sentAt: event.sentAt,
428
440
  ...(event.attachments === undefined ? {} : { attachments: [...event.attachments] }),
441
+ ...(event.messageId === undefined ? {} : { messageId: event.messageId }),
429
442
  });
430
443
  // Anchors pair by position with the checkpoints only a person's steer reserves.
431
444
  if (event.voice === undefined) {
@@ -461,9 +474,9 @@ export class TranscriptFold {
461
474
  );
462
475
  }
463
476
 
464
- // Attended refusals only: the composer holds those words for another press, so a bubble left standing repeats them
465
- // once per press. Splicing is safe because nothing ran — no open bubble, no parked card, no steer above it. A turn
466
- // the sandbox kept (`held`) keeps its message and is recorded: no composer anywhere holds a copy of it.
477
+ // Attended refusals only: the conversation's queue holds those words for another press, so a bubble left standing
478
+ // repeats them once per press. Splicing is safe because nothing ran — no open bubble, no parked card, no steer above
479
+ // it. A turn the sandbox kept (`held`) keeps its message and is recorded: no queue holds a copy of it.
467
480
  private retract(event: Extract<AgentEvent, { kind: "error" }>): TranscriptPatch[] {
468
481
  if (!this.turnedAway(event) || event.held !== undefined) {
469
482
  return [];