@intentic/sandbox-contract 1.308.2 → 1.310.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 (239) 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 +57 -15
  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 +1114 -84
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +2 -3
  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/protocol/upgrade-relay.d.ts +4 -0
  75. package/dist/protocol/upgrade-relay.d.ts.map +1 -0
  76. package/dist/protocol/upgrade-relay.js +25 -0
  77. package/dist/protocol/upgrade-relay.js.map +1 -0
  78. package/dist/schemas/agent.d.ts +59 -0
  79. package/dist/schemas/agent.d.ts.map +1 -1
  80. package/dist/schemas/agent.js +45 -2
  81. package/dist/schemas/agent.js.map +1 -1
  82. package/dist/schemas/agents.d.ts +60 -0
  83. package/dist/schemas/agents.d.ts.map +1 -1
  84. package/dist/schemas/agents.js +6 -1
  85. package/dist/schemas/agents.js.map +1 -1
  86. package/dist/schemas/approvals.d.ts +67 -0
  87. package/dist/schemas/approvals.d.ts.map +1 -1
  88. package/dist/schemas/approvals.js +43 -0
  89. package/dist/schemas/approvals.js.map +1 -1
  90. package/dist/schemas/automations.d.ts +20 -0
  91. package/dist/schemas/automations.d.ts.map +1 -1
  92. package/dist/schemas/ci.d.ts.map +1 -1
  93. package/dist/schemas/ci.js.map +1 -1
  94. package/dist/schemas/devices.d.ts +2 -0
  95. package/dist/schemas/devices.d.ts.map +1 -1
  96. package/dist/schemas/devices.js +1 -0
  97. package/dist/schemas/devices.js.map +1 -1
  98. package/dist/schemas/extension-updates.d.ts +850 -77
  99. package/dist/schemas/extension-updates.d.ts.map +1 -1
  100. package/dist/schemas/extension-updates.js +23 -0
  101. package/dist/schemas/extension-updates.js.map +1 -1
  102. package/dist/schemas/history.d.ts +1 -0
  103. package/dist/schemas/history.d.ts.map +1 -1
  104. package/dist/schemas/history.js +4 -0
  105. package/dist/schemas/history.js.map +1 -1
  106. package/dist/schemas/metrics.d.ts +257 -0
  107. package/dist/schemas/metrics.d.ts.map +1 -1
  108. package/dist/schemas/metrics.js +73 -3
  109. package/dist/schemas/metrics.js.map +1 -1
  110. package/dist/schemas/providers/plan-limits.d.ts +42 -1
  111. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  112. package/dist/schemas/providers/plan-limits.js +48 -1
  113. package/dist/schemas/providers/plan-limits.js.map +1 -1
  114. package/dist/schemas/providers/usage.d.ts +2 -0
  115. package/dist/schemas/providers/usage.d.ts.map +1 -1
  116. package/dist/schemas/providers/usage.js +2 -0
  117. package/dist/schemas/providers/usage.js.map +1 -1
  118. package/dist/schemas/settings.d.ts +61 -29
  119. package/dist/schemas/settings.d.ts.map +1 -1
  120. package/dist/schemas/settings.js +36 -11
  121. package/dist/schemas/settings.js.map +1 -1
  122. package/dist/schemas/turn-break.d.ts +5 -0
  123. package/dist/schemas/turn-break.d.ts.map +1 -1
  124. package/dist/schemas/turn-break.js +4 -0
  125. package/dist/schemas/turn-break.js.map +1 -1
  126. package/dist/state/definition.d.ts +16 -20
  127. package/dist/state/definition.d.ts.map +1 -1
  128. package/dist/state/history-state.d.ts.map +1 -1
  129. package/dist/state/history-state.js +3 -0
  130. package/dist/state/history-state.js.map +1 -1
  131. package/dist/state/workspace-state.d.ts +0 -7
  132. package/dist/state/workspace-state.d.ts.map +1 -1
  133. package/dist/state/workspace-state.js +0 -8
  134. package/dist/state/workspace-state.js.map +1 -1
  135. package/dist/text/transcript-fold.d.ts +1 -1
  136. package/dist/text/transcript-fold.d.ts.map +1 -1
  137. package/dist/text/transcript-fold.js +19 -9
  138. package/dist/text/transcript-fold.js.map +1 -1
  139. package/package.json +27 -5
  140. package/src/chores/chores.test.ts +0 -1
  141. package/src/chores/digest.test.ts +0 -1
  142. package/src/chores/probes.test.ts +0 -1
  143. package/src/chores/stack.test.ts +0 -1
  144. package/src/chores/verdict.test.ts +0 -1
  145. package/src/contracts/agent.contract.ts +62 -15
  146. package/src/contracts/approvals.contract.ts +35 -1
  147. package/src/contracts/extensions.contract.ts +14 -0
  148. package/src/contracts/system.contract.ts +42 -1
  149. package/src/events/agent-events.ts +10 -2
  150. package/src/events/agent-words.test.ts +0 -1
  151. package/src/events/resume.test.ts +8 -1
  152. package/src/events/resume.ts +4 -0
  153. package/src/events/transcript.ts +13 -2
  154. package/src/events/watch-wake.test.ts +0 -1
  155. package/src/ids/conversation-ids.test.ts +1 -2
  156. package/src/ids/hostnames.test.ts +0 -1
  157. package/src/ids/share-paths.test.ts +0 -1
  158. package/src/ids/tunnel-ids.test.ts +0 -1
  159. package/src/index.ts +2 -3
  160. package/src/models/agent-catalog.test.ts +0 -1
  161. package/src/models/capability-ledger.test.ts +0 -1
  162. package/src/models/model-label.test.ts +23 -0
  163. package/src/models/model-label.ts +29 -0
  164. package/src/models/model-offer.test.ts +0 -1
  165. package/src/models/model-order.test.ts +0 -1
  166. package/src/models/model-pins.test.ts +0 -1
  167. package/src/models/model-pins.ts +0 -6
  168. package/src/models/model-roles.test.ts +0 -1
  169. package/src/models/plan-pools.test.ts +0 -1
  170. package/src/models/provider-specs.test.ts +0 -1
  171. package/src/policy/batch-runs.test.ts +0 -1
  172. package/src/policy/command-classes.test.ts +99 -2
  173. package/src/policy/command-classes.ts +143 -11
  174. package/src/policy/credential-material.test.ts +0 -1
  175. package/src/policy/fence-paths.test.ts +0 -1
  176. package/src/policy/overlay-lint.test.ts +0 -1
  177. package/src/policy/owner-ticket.test.ts +0 -1
  178. package/src/policy/persona-home.test.ts +0 -1
  179. package/src/policy/reserved-servers.test.ts +50 -0
  180. package/src/policy/reserved-servers.ts +48 -0
  181. package/src/policy/safety-policy.test.ts +0 -1
  182. package/src/policy/search-globs.test.ts +0 -1
  183. package/src/policy/turned-away.ts +2 -1
  184. package/src/protocol/container-requirements.test.ts +0 -1
  185. package/src/protocol/ingress-contract.test.ts +0 -1
  186. package/src/protocol/ingress-protocol.test.ts +78 -2
  187. package/src/protocol/ingress-protocol.ts +8 -1
  188. package/src/protocol/peer-dial.test.ts +1 -2
  189. package/src/protocol/peer-mcp-server.test.ts +10 -4
  190. package/src/protocol/peer-mcp-server.ts +14 -1
  191. package/src/protocol/routes.test.ts +6 -3
  192. package/src/protocol/tunnel-heartbeat.test.ts +75 -0
  193. package/src/protocol/tunnel-heartbeat.ts +67 -0
  194. package/src/protocol/upgrade-relay.ts +28 -0
  195. package/src/schemas/agent.test.ts +0 -1
  196. package/src/schemas/agent.ts +77 -5
  197. package/src/schemas/agents.ts +11 -1
  198. package/src/schemas/approvals.ts +67 -0
  199. package/src/schemas/areas.test.ts +1 -2
  200. package/src/schemas/ci.ts +3 -9
  201. package/src/schemas/context-trim.test.ts +2 -3
  202. package/src/schemas/dev-rebuild-log.test.ts +0 -1
  203. package/src/schemas/devices.ts +2 -0
  204. package/src/schemas/extension-updates.ts +30 -0
  205. package/src/schemas/git/git.test.ts +0 -1
  206. package/src/schemas/history.ts +8 -1
  207. package/src/schemas/metrics.ts +121 -4
  208. package/src/schemas/providers/plan-limits.ts +67 -2
  209. package/src/schemas/providers/usage.ts +3 -0
  210. package/src/schemas/settings.test.ts +41 -7
  211. package/src/schemas/settings.ts +64 -44
  212. package/src/schemas/turn-break.ts +8 -0
  213. package/src/schemas/version-seam.test.ts +0 -1
  214. package/src/state/contract-lock.test.ts +0 -1
  215. package/src/state/fix-attempt-plan.test.ts +0 -1
  216. package/src/state/fix-stance.test.ts +0 -1
  217. package/src/state/history-state.ts +6 -0
  218. package/src/state/landed-commit.test.ts +0 -1
  219. package/src/state/runtime-state.test.ts +0 -1
  220. package/src/state/versions.test.ts +0 -1
  221. package/src/state/workspace-state.test.ts +2 -5
  222. package/src/state/workspace-state.ts +0 -14
  223. package/src/text/archives.test.ts +0 -1
  224. package/src/text/documents.test.ts +0 -1
  225. package/src/text/embed.test.ts +2 -3
  226. package/src/text/emoji.test.ts +0 -1
  227. package/src/text/mentions.test.ts +0 -1
  228. package/src/text/path-refs.test.ts +0 -1
  229. package/src/text/title.test.ts +0 -1
  230. package/src/text/transcript-fold.test.ts +35 -8
  231. package/src/text/transcript-fold.ts +32 -19
  232. package/src/text/whisper.test.ts +0 -1
  233. package/src/text/workflow-faults.test.ts +0 -1
  234. package/src/time/zone.test.ts +0 -1
  235. package/dist/contracts/prepush.contract.d.ts +0 -32
  236. package/dist/contracts/prepush.contract.d.ts.map +0 -1
  237. package/dist/contracts/prepush.contract.js +0 -41
  238. package/dist/contracts/prepush.contract.js.map +0 -1
  239. package/src/contracts/prepush.contract.ts +0 -58
@@ -1,6 +1,5 @@
1
1
  import { STATE_DIR } from "@intentic/constants";
2
2
  import type { FileContribution } from "@intentic/extension-manifest";
3
- import { describe, it, expect } from "bun:test";
4
3
  import {
5
4
  BACKED_UP_STATE_PATHS,
6
5
  isLockedWorkspacePath,
@@ -60,8 +59,8 @@ describe(`staleQueryKeys`, () => {
60
59
  });
61
60
 
62
61
  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([]);
62
+ // writeTextFile's atomic-rename temp name: `.<name>.<pid>.<n>.tmp` beside the target.
63
+ expect(staleQueryKeys([`.intentic/.settings.json.42.7.tmp`], [])).toEqual([]);
65
64
  expect(staleQueryKeys([`.intentic/config/settings.json`], [])).toEqual([`settings`, `manifests`]);
66
65
  });
67
66
 
@@ -346,8 +345,6 @@ describe(`VERSIONED_STATE_PATHS`, () => {
346
345
  // Which commands count as heavy, and how many may run at once; a shared-box decision worth a `git log`
347
346
  // line.
348
347
  `${STATE_DIR}/config/heavy-commands.json`,
349
- // The exclude list carves entries out by name; an unlisted directory is invisible to git and to land.
350
- `${STATE_DIR}/config/hooks/`,
351
348
  `${STATE_DIR}/config/loop-designs.json`,
352
349
  `${STATE_DIR}/config/personas.json`,
353
350
  // A persona's prompt and reachable skills: the text that decides how it behaves, same reason as its card.
@@ -120,20 +120,6 @@ const STATE_FILES = [
120
120
  // Bash command. `carry`: a property of the workspace, not the machine. `versioned`: raising the limit is a decision
121
121
  // worth a diff.
122
122
  { path: ".intentic/config/heavy-commands.json", invalidates: ["settings"], portability: "carry", versioned: true },
123
- // Scripts the rule table runs, one per reader (settings.json points at them per event, e.g. `file.edited`).
124
- // `versioned`: this is code that runs against every file an agent writes, more consequential than the rule naming
125
- // it. `carry`: authored text, no credential. Invalidates nothing: the settings screen renders the rules from
126
- // settings.json, not these scripts.
127
- {
128
- path: ".intentic/config/hooks/",
129
- invalidates: [],
130
- why: "The settings screen renders the rules that name these scripts, out of settings.json; nothing in the browser reads the scripts themselves.",
131
- portability: "carry",
132
- versioned: true,
133
- // The daemon only runs these scripts by path; it never builds them, so there's no `statePath` call for the
134
- // coverage guard to find.
135
- outsideWriter: "the owner or an agent, authoring them; the daemon only ever RUNS one, by the path a rule's command names",
136
- },
137
123
  // Last-fired stamps for the rule table. `derived`: records what happened on this machine, not carried.
138
124
  {
139
125
  path: ".intentic/local/rule-firings.json",
@@ -1,4 +1,3 @@
1
- import { describe, it, expect } from "bun:test";
2
1
  import { archiveFormat, archivePrefixOf, archiveRootOf, archiveStem, isBrowsableArchive, wrapsItsOwnName } from "./archives.js";
3
2
 
4
3
  describe(`archive names`, () => {
@@ -1,4 +1,3 @@
1
- import { describe, test, expect } from "bun:test";
2
1
  import { documentOf, documentTitle, isDocumentPath, isPlanDocumentPath } from "./documents.js";
3
2
  import { PLAN_DOCUMENTS_DIR } from "../state/workspace-state.js";
4
3
 
@@ -1,5 +1,4 @@
1
1
  import { webcrypto } from "node:crypto";
2
- import { test, expect, beforeAll, mock } from "bun:test";
3
2
  import { stubGlobal, unstubAllGlobals } from "@intentic/testing/bun";
4
3
  import { EmbedError, embedFailure, embedUrl, fetchEmbedJson, solveProofOfWork } from "./embed.js";
5
4
 
@@ -40,12 +39,12 @@ test("a refusal carries the server's own sentence and status, and a bodyless one
40
39
  test("a JSON fetch answers the body or throws the refusal", async () => {
41
40
  stubGlobal(
42
41
  "fetch",
43
- mock(async () => new Response(JSON.stringify({ ok: true }), { status: 200 })),
42
+ jest.fn(async () => new Response(JSON.stringify({ ok: true }), { status: 200 })),
44
43
  );
45
44
  expect(await fetchEmbedJson<{ ok: boolean }>("https://x/y")).toEqual({ ok: true });
46
45
  stubGlobal(
47
46
  "fetch",
48
- mock(async () => new Response(JSON.stringify({ error: "rate limited" }), { status: 429 })),
47
+ jest.fn(async () => new Response(JSON.stringify({ error: "rate limited" }), { status: 429 })),
49
48
  );
50
49
  await expect(fetchEmbedJson("https://x/y")).rejects.toMatchObject({ message: "rate limited", status: 429 });
51
50
  unstubAllGlobals();
@@ -1,4 +1,3 @@
1
- import { describe, it, expect } from "bun:test";
2
1
  import { isSingleEmoji } from "./emoji.js";
3
2
 
4
3
  describe("isSingleEmoji", () => {
@@ -1,4 +1,3 @@
1
- import { describe, it, expect } from "bun:test";
2
1
  import { AgentTurnSchema } from "../schemas/agent.js";
3
2
  import { MENTION_LIMIT, mentionedPathTokens, mentionPaths } from "./mentions.js";
4
3
 
@@ -1,4 +1,3 @@
1
- import { describe, test, expect } from "bun:test";
2
1
  import { isTestPath, rankRefCandidates, referenceTails } from "./path-refs.js";
3
2
 
4
3
  describe("referenceTails", () => {
@@ -1,4 +1,3 @@
1
- import { test, expect } from "bun:test";
2
1
  import { AgentTurnSchema } from "../schemas/agent.js";
3
2
  import { deriveTitle } from "./title.js";
4
3
 
@@ -1,5 +1,4 @@
1
1
  import { STATE_DIR } from "@intentic/constants";
2
- import { describe, it, expect } from "bun:test";
3
2
  import type { AgentEvent } from "../events/agent-events.js";
4
3
  import type { TranscriptPatch, TranscriptRow } from "../events/transcript.js";
5
4
  import { watchWakePrompt } from "../events/watch-wake.js";
@@ -186,7 +185,8 @@ describe("foldTurn", () => {
186
185
  code: "provider-outage",
187
186
  message: "Anthropic is down.",
188
187
  autoResume: "scheduled",
189
- outage: { retryAt: 1, attempt: 2, maxAttempts: 6 },
188
+ outage: { retryAt: 1 },
189
+ retries: { made: 1, max: 6 },
190
190
  },
191
191
  ];
192
192
  // The row states what happened and nothing more: what happens next, and the one control that changes it, are
@@ -200,6 +200,33 @@ describe("foldTurn", () => {
200
200
  });
201
201
  });
202
202
 
203
+ // The count lives in the record, so a reader scrolling back sees how many times a stuck turn was sent again.
204
+ it("says on a stopped turn's row which automatic try comes next, or that the ladder stood down", () => {
205
+ const timedOut = "Google turn timed out waiting for OpenCode.";
206
+ const booked: AgentEvent[] = [{ kind: "error", message: timedOut, autoResume: "scheduled", nextAt: 1, retries: { made: 1, max: 3 } }];
207
+ expect(foldOf("hi", booked).at(-1)).toEqual({ role: "notice", text: `${timedOut} Retrying by itself: attempt 2 of 3.` });
208
+ const spent: AgentEvent[] = [{ kind: "error", message: timedOut, autoResume: "available", retries: { made: 3, max: 3 } }];
209
+ expect(foldOf("hi", spent).at(-1)).toEqual({
210
+ role: "notice",
211
+ text: `${timedOut} Retried 3 of 3 times by itself; nothing more is sent automatically.`,
212
+ });
213
+ // An outage nobody armed says so, instead of promising the breaker's retry.
214
+ const unarmed: AgentEvent[] = [
215
+ {
216
+ kind: "error",
217
+ code: "provider-outage",
218
+ message: "Anthropic is down.",
219
+ autoResume: "available",
220
+ outage: { retryAt: 1 },
221
+ retries: { made: 0, max: 6 },
222
+ },
223
+ ];
224
+ expect(foldOf("hi", unarmed).at(-1)).toEqual({
225
+ role: "notice",
226
+ text: "Anthropic is down. Nothing is retrying it, so the turn is waiting here.",
227
+ });
228
+ });
229
+
203
230
  it("writes the turn's own events down as notices", () => {
204
231
  const events: AgentEvent[] = [
205
232
  { kind: "worktree", branch: "agent/x", base: "abc1234", sync: { commits: 2, blocked: [] } },
@@ -213,7 +240,7 @@ describe("foldTurn", () => {
213
240
  { role: "notice", text: "Context compacted to free up space." },
214
241
  {
215
242
  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.",
243
+ 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
244
  noticeAction: "depsInstall",
218
245
  },
219
246
  ]);
@@ -495,8 +522,8 @@ describe("patches", () => {
495
522
  });
496
523
  });
497
524
 
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.
525
+ // A refusal that ran nothing offers the message back for another press — true where somebody typed one, false for a
526
+ // run that started itself, where there is no composer and no typed message.
500
527
  describe(`a refusal that ran nothing`, () => {
501
528
  const REFUSED = { kind: `error`, code: `context-window-too-small`, message: `This model accepts 16,384 tokens.` } as const satisfies AgentEvent;
502
529
 
@@ -551,9 +578,9 @@ describe(`a refusal that ran nothing`, () => {
551
578
  expect(foldOf(`land it`, [REFUSED]).at(-1)?.noticeAction).toBeUndefined();
552
579
  });
553
580
 
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`, () => {
581
+ // The bug this retraction exists for: the conversation's queue keeps the words for another press, so a bubble left
582
+ // standing here is the SAME message a second time, and a third, once per press against a gate that keeps refusing.
583
+ it(`takes the message back out, leaving the queue's copy as the only one`, () => {
557
584
  const fold = new TranscriptFold(openingOf(`land it`));
558
585
  const patches = fold.apply(REFUSED);
559
586
 
@@ -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 [];
@@ -1,4 +1,3 @@
1
- import { test, expect } from "bun:test";
2
1
  import { cleanTranscription } from "./whisper.js";
3
2
 
4
3
  test("flattens whisper output onto one line", () => {
@@ -1,4 +1,3 @@
1
- import { test, expect } from "bun:test";
2
1
  import { type Workflow, WorkflowSchema, type WorkflowStep } from "../schemas/workflows.js";
3
2
  import { workflowFaults, workflowRunFaults } from "./workflow-faults.js";
4
3
 
@@ -1,4 +1,3 @@
1
- import { describe, test, expect } from "bun:test";
2
1
  import { asCivilDay, asZone, civilDayIn, cronOptions, isZone, sameClock, utcDayOf, UTC, zoneLabel } from "./zone.js";
3
2
  import type { Zone } from "./zone.js";
4
3
 
@@ -1,32 +0,0 @@
1
- import { z } from "zod";
2
- export declare const PrepushRunSchema: z.ZodPrefault<z.ZodObject<{
3
- repos: z.ZodDefault<z.ZodArray<z.ZodString>>;
4
- }, z.core.$strip>>;
5
- export declare const prepushContract: {
6
- state: import("@orpc/contract").ContractProcedureBuilderWithOutput<import("@orpc/contract").Schema<unknown, unknown>, z.ZodObject<{
7
- status: z.ZodEnum<{
8
- cancelled: "cancelled";
9
- error: "error";
10
- failed: "failed";
11
- idle: "idle";
12
- passed: "passed";
13
- running: "running";
14
- }>;
15
- command: z.ZodString;
16
- startedAt: z.ZodOptional<z.ZodNumber>;
17
- finishedAt: z.ZodOptional<z.ZodNumber>;
18
- exitCode: z.ZodOptional<z.ZodNumber>;
19
- timedOut: z.ZodOptional<z.ZodBoolean>;
20
- session: z.ZodOptional<z.ZodString>;
21
- output: z.ZodString;
22
- }, z.core.$strip>, Record<never, never>, import("../protocol/route-meta.js").RouteMeta & Record<never, never>>;
23
- run: import("@orpc/contract").ContractProcedureBuilderWithInputOutput<z.ZodPrefault<z.ZodObject<{
24
- repos: z.ZodDefault<z.ZodArray<z.ZodString>>;
25
- }, z.core.$strip>>, z.ZodObject<{
26
- ok: z.ZodLiteral<true>;
27
- }, z.core.$strip>, Record<never, never>, import("../protocol/route-meta.js").RouteMeta & Record<never, never>>;
28
- cancel: import("@orpc/contract").ContractProcedureBuilderWithOutput<import("@orpc/contract").Schema<unknown, unknown>, z.ZodObject<{
29
- ok: z.ZodLiteral<true>;
30
- }, z.core.$strip>, Record<never, never>, import("../protocol/route-meta.js").RouteMeta & Record<never, never>>;
31
- };
32
- //# sourceMappingURL=prepush.contract.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"prepush.contract.d.ts","sourceRoot":"","sources":["../../src/contracts/prepush.contract.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AASxB,eAAO,MAAM,gBAAgB;;kBAQZ,CAAC;AAWlB,eAAO,MAAM,eAAe;IACxB,KAAK;;;;;;;;;;;;;;;;;IASL,GAAG;;;;;IAUH,MAAM;;;CAQT,CAAC"}
@@ -1,41 +0,0 @@
1
- import { procedure } from "../protocol/route-meta.js";
2
- import { z } from "zod";
3
- import { CommandRunSchema } from "../schemas/ci.js";
4
- import { OkSchema } from "../schemas/shared.js";
5
- export const PrepushRunSchema = z
6
- .object({
7
- repos: z
8
- .array(z.string().min(1))
9
- .max(100)
10
- .default([])
11
- .describe("The repositories going out, by workspace id. Empty runs only what stands for every push, whichever repository it is."),
12
- })
13
- .prefault({});
14
- export const prepushContract = {
15
- state: procedure
16
- .route({
17
- method: "GET",
18
- path: "/prepush/state",
19
- summary: "How the pre-push check is going",
20
- description: "The verdict, or the progress so far. Nothing is addressed by id here, because there is one working tree and so exactly one check.",
21
- })
22
- .output(CommandRunSchema),
23
- run: procedure
24
- .route({
25
- method: "POST",
26
- path: "/prepush/run",
27
- summary: "Run the checks before pushing",
28
- description: "Starts the suite the workspace runs before anything leaves the machine, and answers immediately. A suite takes minutes, and a request held open that long dies at the first proxy. It runs in a real terminal, so watch it there and poll for the verdict. Name the repositories going out, and each one's own checks run in its own directory.",
29
- })
30
- .input(PrepushRunSchema)
31
- .output(OkSchema),
32
- cancel: procedure
33
- .route({
34
- method: "POST",
35
- path: "/prepush/cancel",
36
- summary: "Stop the pre-push check",
37
- description: "Kills the run. It settles as cancelled and the push it was gating does not go.",
38
- })
39
- .output(OkSchema),
40
- };
41
- //# sourceMappingURL=prepush.contract.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"prepush.contract.js","sourceRoot":"","sources":["../../src/contracts/prepush.contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAOhD,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC;KAC5B,MAAM,CAAC;IACJ,KAAK,EAAE,CAAC;SACH,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;SACxB,GAAG,CAAC,GAAG,CAAC;SACR,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,sHAAsH,CAAC;CACxI,CAAC;KACD,QAAQ,CAAC,EAAE,CAAC,CAAC;AAWlB,MAAM,CAAC,MAAM,eAAe,GAAG;IAC3B,KAAK,EAAE,SAAS;SACX,KAAK,CAAC;QACH,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,gBAAgB;QACtB,OAAO,EAAE,iCAAiC;QAC1C,WAAW,EACP,mIAAmI;KAC1I,CAAC;SACD,MAAM,CAAC,gBAAgB,CAAC;IAC7B,GAAG,EAAE,SAAS;SACT,KAAK,CAAC;QACH,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,cAAc;QACpB,OAAO,EAAE,+BAA+B;QACxC,WAAW,EACP,iVAAiV;KACxV,CAAC;SACD,KAAK,CAAC,gBAAgB,CAAC;SACvB,MAAM,CAAC,QAAQ,CAAC;IACrB,MAAM,EAAE,SAAS;SACZ,KAAK,CAAC;QACH,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,iBAAiB;QACvB,OAAO,EAAE,yBAAyB;QAClC,WAAW,EAAE,gFAAgF;KAChG,CAAC;SACD,MAAM,CAAC,QAAQ,CAAC;CACxB,CAAC"}
@@ -1,58 +0,0 @@
1
- import { procedure } from "../protocol/route-meta.js";
2
- import { z } from "zod";
3
- import { CommandRunSchema } from "../schemas/ci.js";
4
- import { OkSchema } from "../schemas/shared.js";
5
-
6
- // Which repositories are about to be pushed. The check is one run over one working tree, but what stands in it is not
7
- // the same for every push: a repository's own checks (repo-checks.ts) run only when that repository is going out, and a
8
- // rule the owner aimed at one repository does the same.
9
- // `prefault`, so a caller that sends no body at all still starts the check: a browser tab open since before this input
10
- // existed is the ordinary case, and refusing its push over a missing field would be the worst possible way to say so.
11
- export const PrepushRunSchema = z
12
- .object({
13
- repos: z
14
- .array(z.string().min(1))
15
- .max(100)
16
- .default([])
17
- .describe("The repositories going out, by workspace id. Empty runs only what stands for every push, whichever repository it is."),
18
- })
19
- .prefault({});
20
-
21
- // The pre-push check, the command the workspace runs when the user pushes, before anything leaves the machine
22
- // (see CommandRunSchema for where this sits and why). Three verbs about ONE run: the check answers about the
23
- // main working tree, of which there is exactly one, so nothing here is addressed by id.
24
- //
25
- // `run` starts the check and returns immediately, a suite takes minutes, and an oRPC call held open for one
26
- // would die on the first proxy timeout with the work still going. The dialog that started it polls `state` for
27
- // the verdict, and opens the terminals panel on the `session` the first answer names: the suite runs in a real
28
- // tmux window, so watching it is the terminal's job. `cancel` kills that window; the run settles as `cancelled`
29
- // and the push it was gating does not go.
30
- export const prepushContract = {
31
- state: procedure
32
- .route({
33
- method: "GET",
34
- path: "/prepush/state",
35
- summary: "How the pre-push check is going",
36
- description:
37
- "The verdict, or the progress so far. Nothing is addressed by id here, because there is one working tree and so exactly one check.",
38
- })
39
- .output(CommandRunSchema),
40
- run: procedure
41
- .route({
42
- method: "POST",
43
- path: "/prepush/run",
44
- summary: "Run the checks before pushing",
45
- description:
46
- "Starts the suite the workspace runs before anything leaves the machine, and answers immediately. A suite takes minutes, and a request held open that long dies at the first proxy. It runs in a real terminal, so watch it there and poll for the verdict. Name the repositories going out, and each one's own checks run in its own directory.",
47
- })
48
- .input(PrepushRunSchema)
49
- .output(OkSchema),
50
- cancel: procedure
51
- .route({
52
- method: "POST",
53
- path: "/prepush/cancel",
54
- summary: "Stop the pre-push check",
55
- description: "Kills the run. It settles as cancelled and the push it was gating does not go.",
56
- })
57
- .output(OkSchema),
58
- };