@intentic/sandbox-contract 1.167.0 → 1.169.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 (68) hide show
  1. package/dist/agent-catalog.d.ts +20 -2
  2. package/dist/agent-catalog.d.ts.map +1 -1
  3. package/dist/agent-catalog.js +80 -4
  4. package/dist/agent-catalog.js.map +1 -1
  5. package/dist/contracts/agent.contract.d.ts +36 -8
  6. package/dist/contracts/agent.contract.d.ts.map +1 -1
  7. package/dist/contracts/agent.contract.js +1 -2
  8. package/dist/contracts/agent.contract.js.map +1 -1
  9. package/dist/contracts/agents.contract.d.ts +37 -39
  10. package/dist/contracts/agents.contract.d.ts.map +1 -1
  11. package/dist/contracts/extensions.contract.d.ts +4 -0
  12. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  13. package/dist/contracts/prepush.contract.d.ts +25 -0
  14. package/dist/contracts/prepush.contract.d.ts.map +1 -0
  15. package/dist/contracts/prepush.contract.js +8 -0
  16. package/dist/contracts/prepush.contract.js.map +1 -0
  17. package/dist/contracts/sessions.contract.d.ts +1 -39
  18. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  19. package/dist/contracts/settings.contract.d.ts +8 -10
  20. package/dist/contracts/settings.contract.d.ts.map +1 -1
  21. package/dist/contracts/system.contract.d.ts +56 -0
  22. package/dist/contracts/system.contract.d.ts.map +1 -1
  23. package/dist/contracts/system.contract.js +7 -2
  24. package/dist/contracts/system.contract.js.map +1 -1
  25. package/dist/contracts/translator.contract.d.ts +36 -0
  26. package/dist/contracts/translator.contract.d.ts.map +1 -1
  27. package/dist/events.d.ts +88 -159
  28. package/dist/events.d.ts.map +1 -1
  29. package/dist/events.js +37 -4
  30. package/dist/events.js.map +1 -1
  31. package/dist/index.d.ts +218 -157
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +4 -3
  34. package/dist/index.js.map +1 -1
  35. package/dist/quick-model.d.ts +1 -0
  36. package/dist/quick-model.d.ts.map +1 -1
  37. package/dist/quick-model.js +1 -1
  38. package/dist/quick-model.js.map +1 -1
  39. package/dist/schemas.d.ts +176 -69
  40. package/dist/schemas.d.ts.map +1 -1
  41. package/dist/schemas.js +48 -40
  42. package/dist/schemas.js.map +1 -1
  43. package/dist/title.d.ts.map +1 -1
  44. package/dist/title.js.map +1 -1
  45. package/dist/workspace-state.d.ts +9 -0
  46. package/dist/workspace-state.d.ts.map +1 -0
  47. package/dist/workspace-state.js +61 -0
  48. package/dist/workspace-state.js.map +1 -0
  49. package/package.json +2 -2
  50. package/src/agent-catalog.test.ts +118 -0
  51. package/src/agent-catalog.ts +179 -15
  52. package/src/contracts/agent.contract.ts +1 -14
  53. package/src/contracts/prepush.contract.ts +16 -0
  54. package/src/contracts/system.contract.ts +15 -1
  55. package/src/events.test.ts +32 -0
  56. package/src/events.ts +105 -27
  57. package/src/index.ts +4 -3
  58. package/src/quick-model.ts +4 -2
  59. package/src/schemas.test.ts +13 -18
  60. package/src/schemas.ts +179 -152
  61. package/src/title.ts +6 -2
  62. package/src/workspace-state.test.ts +129 -0
  63. package/src/workspace-state.ts +150 -0
  64. package/dist/contracts/gate.contract.d.ts +0 -47
  65. package/dist/contracts/gate.contract.d.ts.map +0 -1
  66. package/dist/contracts/gate.contract.js +0 -9
  67. package/dist/contracts/gate.contract.js.map +0 -1
  68. package/src/contracts/gate.contract.ts +0 -19
package/src/events.ts CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  LandConflictSchema,
7
7
  PermissionModeSchema,
8
8
  RateLimitInfoSchema,
9
+ SubagentKindSchema,
10
+ SubagentStatusSchema,
9
11
  UsageWindowSchema,
10
12
  } from "./schemas.js";
11
13
 
@@ -125,19 +127,36 @@ export type ToolCallContent = z.infer<typeof ToolCallContentSchema>;
125
127
  // tool_use/tool_result blocks, so a restored card carries everything the live `tool_call` frame did except
126
128
  // the streaming-only correlation fields.
127
129
  //
128
- // One restored tool card. Subagent (Task) calls do NOT nest here: the SDK stores a delegation's own calls in a
129
- // separate per-subagent file, so a Task card restores as a leaf and its children stay collapsed the live
130
- // stream still nests them (see ChatTool.children).
131
- export const RestoredToolCallSchema = z.object({
132
- id: z.string(),
133
- name: z.string(),
134
- category: ToolKindSchema,
135
- status: ToolCallStatusSchema,
136
- target: z.string().optional(),
137
- locations: z.array(ToolCallLocationSchema).optional(),
138
- content: z.array(ToolCallContentSchema).optional(),
139
- });
140
- export type RestoredToolCall = z.infer<typeof RestoredToolCallSchema>;
130
+ // One restored tool card. A subagent's own calls and its thinking nest under the Agent card that spawned them,
131
+ // the same two fields (and the same recursion) the live ChatTool carries so a reopened chat redraws the
132
+ // delegation it was showing instead of a leaf card with the whole child collapsed into its result text.
133
+ // z.lazy because the shape refers to itself: a subagent that delegates nests one level deeper.
134
+ export const RestoredToolCallSchema: z.ZodType<RestoredToolCall> = z.lazy(() =>
135
+ z.object({
136
+ id: z.string(),
137
+ name: z.string(),
138
+ category: ToolKindSchema,
139
+ status: ToolCallStatusSchema,
140
+ target: z.string().optional(),
141
+ locations: z.array(ToolCallLocationSchema).optional(),
142
+ content: z.array(ToolCallContentSchema).optional(),
143
+ children: z.array(RestoredToolCallSchema).optional(),
144
+ thinking: z.string().optional(),
145
+ }),
146
+ );
147
+ // Mutable, unlike most of this file: both builders settle a card IN PLACE when its result arrives turns later
148
+ // (restoredTurn's `cards` map, readWorkspaceSession's `awaiting`), which is what saves them a second pass.
149
+ export interface RestoredToolCall {
150
+ id: string;
151
+ name: string;
152
+ category: ToolKind;
153
+ status: ToolCallStatus;
154
+ target?: string | undefined;
155
+ locations?: ToolCallLocation[] | undefined;
156
+ content?: ToolCallContent[] | undefined;
157
+ children?: RestoredToolCall[] | undefined;
158
+ thinking?: string | undefined;
159
+ }
141
160
 
142
161
  // One restored bubble. Each stored assistant message becomes its own, which is what reproduces the live
143
162
  // interleaving — prose, the tool cards that prose introduced, then the next block of prose — rather than
@@ -162,7 +181,8 @@ export const AgentTranscriptSchema = SessionTranscriptSchema.extend({ sessionId:
162
181
  // without a UI mapping is dropped. `plan`/`question`/`permission` pause the turn until the user answers on the
163
182
  // `POST /agent/reply` side channel, and `resolved` releases the one it names; `mode` reports the live
164
183
  // permission posture as the agent changes it.
165
- // `parentToolUseId` tags frames produced inside a subagent (Task tool).
184
+ // `parentToolUseId` tags frames produced inside a subagent (Task tool); `subagent`/`subagent_update` report the
185
+ // subagent itself, keyed by the same tool_use id those tagged frames carry.
166
186
  export const AgentEventSchema = z.discriminatedUnion("kind", [
167
187
  z.object({ kind: z.literal("session"), sessionId: z.string() }),
168
188
  /* First frame of an isolated turn: the conversation's worktree identity — its branch (agent/<id>) and the
@@ -229,6 +249,40 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
229
249
  // `browser-<id>` session, and the client surfaces it in the same panel as the terminals. One per turn, for
230
250
  // the same reason: one browser serves every browser call the turn makes.
231
251
  z.object({ kind: z.literal("browser"), session: z.string() }),
252
+ /* THE AGENT STARTED ANOTHER AGENT — an Agent/Task subagent, or a Codex/Grok CLI it drove from its own Bash
253
+ * (see SubagentSessionSchema). One `subagent` frame per child, then `subagent_update` as it works: the same
254
+ * call/update pair `tool_call`/`tool_call_update` uses, and for the same reason — the fields that move
255
+ * (status, spend, what it is doing) arrive many times and must REPLACE, while the fields that identify it are
256
+ * said once.
257
+ *
258
+ * `id` is the SPAWNING TOOL CALL's id — the same id the client already nests the child's inner frames under
259
+ * (`parentToolUseId`), so both frames land on the card that spawned the child by the lookup that is already
260
+ * there (mapToolAnywhere). No second correlation, and nothing to get wrong.
261
+ *
262
+ * These exist because the SDK's task messages were dropped. A BACKGROUNDED child (the Agent tool's default)
263
+ * emits its tool_use and then nothing until its result lands, which for a long child is minutes of a spinner
264
+ * that cannot say whether anything is happening. */
265
+ z.object({
266
+ kind: z.literal("subagent"),
267
+ id: z.string(),
268
+ subagentKind: SubagentKindSchema,
269
+ agentType: z.string().optional(),
270
+ description: z.string().optional(),
271
+ model: z.string().optional(),
272
+ background: z.boolean().optional(),
273
+ // A delegation's tmux session — the one live view a subagent doesn't have (SubagentSessionSchema).
274
+ terminal: z.string().optional(),
275
+ }),
276
+ z.object({
277
+ kind: z.literal("subagent_update"),
278
+ id: z.string(),
279
+ status: SubagentStatusSchema.optional(),
280
+ tokens: z.number().optional(),
281
+ toolUses: z.number().optional(),
282
+ lastTool: z.string().optional(),
283
+ summary: z.string().optional(),
284
+ error: z.string().optional(),
285
+ }),
232
286
  z.object({ kind: z.literal("todos"), items: z.array(TodoItemSchema) }),
233
287
  // The provider's own slash commands (ACP available_commands_update), replaced whole each time — the
234
288
  // composer's `/` popover lists them; invoking one is plain `/name …` prompt text (the ACP convention).
@@ -322,9 +376,11 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
322
376
  "claude-reauth",
323
377
  // The API refused this turn's token MID-FLIGHT — nearly always one superseded by a rotation,
324
378
  // which Anthropic retires the moment its successor is minted. Distinct from claude-reauth: the
325
- // account is fine and the daemon re-mints on the spot, so this frame is a notice about a turn
326
- // that resumed itself, not a request for the user to do anything. It only reaches the client
327
- // when the resume could NOT start, which is when reconnecting really is the fix.
379
+ // account is fine and the daemon re-mints on the spot, so this is usually a notice about a turn
380
+ // that is coming back rather than a request for the user to do anything. `autoResume` says
381
+ // which of the two: "scheduled" means the re-mint-and-re-run is armed, and its absence means
382
+ // nothing is coming (the turn was already a resume, or it ran on a credential with nothing to
383
+ // re-mint from) — that is the case where reconnecting really is the fix.
328
384
  "claude-token-refused",
329
385
  // The model provider itself failed transiently — 500/502/503, a 529 at capacity, a dropped
330
386
  // socket — and the harness's own in-turn retries did not outlast it. Nothing about the workspace
@@ -346,12 +402,12 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
346
402
  // rate_limit_event or the account's persisted usage windows). Absent when the reset instant is unknown
347
403
  // (nothing to schedule against).
348
404
  resetsAt: z.number().optional(),
349
- // Where the daemon's resume of THIS turn stands the same two states for a spent allowance and for a
350
- // provider outage, because the client's reading of them is the same: "scheduled" = the resume is armed
351
- // and this turn comes back by itself; "available" = the daemon remembered the failed turn and turning
352
- // the setting on (autoResumeOnLimit / resumeAfterOutage) arms that same resume, which is what the
353
- // chat's offer banner hangs off. Absent normally means there is nothing automatic to resume; the
354
- // usage-limit feature gate also leaves it absent while preserving the explicit account-switch path.
405
+ // Where the daemon's resume of THIS turn stands, for the two codes that have one (provider-outage,
406
+ // claude-token-refused). "scheduled" = the resume is armed and this turn comes back by itself;
407
+ // "available" = the daemon remembered the failed turn and turning resumeAfterOutage on arms that same
408
+ // resume, which is what the chat's offer banner hangs off outage only, since a renewal is never gated
409
+ // on a setting. Absent means there is nothing automatic to resume: a spent usage limit never has one,
410
+ // and a refused credential has none once re-minting it has already been tried and failed.
355
411
  autoResume: z.enum(["scheduled", "available"]).optional(),
356
412
  /* provider-outage only: the shape of the wait. `retryAt` (epoch seconds) is when the next attempt is
357
413
  * due — not a fixed cadence, because an outage has no reset instant to aim at and hammering a provider
@@ -362,10 +418,6 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
362
418
  * on-by-default retry that gives no account of how long it will keep going is the kind users switch off
363
419
  * defensively; one that says "attempt 2 of 6" is one they leave on. */
364
420
  outage: z.object({ retryAt: z.number(), attempt: z.number(), maxAttempts: z.number() }).optional(),
365
- // rate_limit only: the account whose allowance is spent, as the DAEMON resolved it (the client's own
366
- // selection can be empty, which means "the provider's first"). It is what lets the chat offer the
367
- // provider's OTHER accounts as a resume-now instead of a wait — see /agent/resume-limit.
368
- account: z.string().optional(),
369
421
  }),
370
422
  z.object({ kind: z.literal("done") }),
371
423
  ]);
@@ -384,6 +436,32 @@ export const AttachFrameSchema = z.discriminatedUnion("kind", [
384
436
  ]);
385
437
  export type AttachFrame = z.infer<typeof AttachFrameSchema>;
386
438
 
439
+ /* WHAT A RESUMED TURN'S PROMPT SAYS IT IS. The daemon re-runs a turn something underneath it killed (turn-resume.ts)
440
+ * by sending the original prompt again behind one of these sentences, so the model knows what interrupted it.
441
+ *
442
+ * They live on the wire rather than in the daemon because the CLIENT has to recognise them too: an attach head
443
+ * carries the run's prompt verbatim, and a window joining a resumed run would otherwise render the note as a
444
+ * message the USER wrote — the same words the user already said one run up, with a machine's preamble on them.
445
+ * Recognising the prefix is what lets that window reuse the bubble that is already there instead. */
446
+ export const RESUME_NOTES = {
447
+ auth: "The Claude credential that interrupted this conversation has been renewed, and this turn resumed automatically.",
448
+ outage: "The model provider was briefly unavailable and interrupted this conversation; this turn resumed automatically.",
449
+ restart: "The sandbox restarted while this turn was running, which stopped it, and this turn resumed automatically once it came back.",
450
+ } as const;
451
+
452
+ // The prompt a resume actually sends: the note, then why the words below are being repeated, then them.
453
+ export const withResumeNote = (prompt: string, note: string): string =>
454
+ Object.values(RESUME_NOTES).some((known) => prompt.startsWith(known))
455
+ ? prompt
456
+ : `${note} The interrupted request is repeated below — where part of it was already completed in this session, continue from that point instead of starting over.\n\n${prompt}`;
457
+
458
+ // The user's own words inside a resumed prompt — the note and its explanation stripped back off. Returns the
459
+ // prompt unchanged when it is not a resume, so a caller can hand every attach head through it.
460
+ export const withoutResumeNote = (prompt: string): string => {
461
+ const note = Object.values(RESUME_NOTES).find((known) => prompt.startsWith(known));
462
+ return note === undefined ? prompt : prompt.slice(prompt.indexOf("\n\n") + 2);
463
+ };
464
+
387
465
  // One parsed line from `intentic … --output ndjson` (engine events, provider `log`, the terminal `result`).
388
466
  // Open-ended by design — the sandbox consumes the wire shape, not @intentic/engine's types — so a string
389
467
  // `kind` plus arbitrary extra fields pass through. The apply-events tail (intentic.contract `applyEvents`) rides
package/src/index.ts CHANGED
@@ -10,7 +10,6 @@ import { claudeContract } from "./contracts/claude.contract.js";
10
10
  import { codexContract } from "./contracts/codex.contract.js";
11
11
  import { draftsContract } from "./contracts/drafts.contract.js";
12
12
  import { extensionsContract } from "./contracts/extensions.contract.js";
13
- import { gateContract } from "./contracts/gate.contract.js";
14
13
  import { geminiContract } from "./contracts/gemini.contract.js";
15
14
  import { gitContract } from "./contracts/git.contract.js";
16
15
  import { grokContract } from "./contracts/grok.contract.js";
@@ -22,6 +21,7 @@ import { logsContract } from "./contracts/logs.contract.js";
22
21
  import { memoryContract } from "./contracts/memory.contract.js";
23
22
  import { panelsContract } from "./contracts/panels.contract.js";
24
23
  import { portsContract } from "./contracts/ports.contract.js";
24
+ import { prepushContract } from "./contracts/prepush.contract.js";
25
25
  import { pushContract } from "./contracts/push.contract.js";
26
26
  import { secretsContract } from "./contracts/secrets.contract.js";
27
27
  import { sessionsContract } from "./contracts/sessions.contract.js";
@@ -42,7 +42,6 @@ export { claudeContract } from "./contracts/claude.contract.js";
42
42
  export { codexContract } from "./contracts/codex.contract.js";
43
43
  export { draftsContract } from "./contracts/drafts.contract.js";
44
44
  export { extensionsContract } from "./contracts/extensions.contract.js";
45
- export { gateContract } from "./contracts/gate.contract.js";
46
45
  export { geminiContract } from "./contracts/gemini.contract.js";
47
46
  export { gitContract } from "./contracts/git.contract.js";
48
47
  export { grokContract } from "./contracts/grok.contract.js";
@@ -54,6 +53,7 @@ export { logsContract } from "./contracts/logs.contract.js";
54
53
  export { memoryContract } from "./contracts/memory.contract.js";
55
54
  export { panelsContract } from "./contracts/panels.contract.js";
56
55
  export { portsContract } from "./contracts/ports.contract.js";
56
+ export { prepushContract } from "./contracts/prepush.contract.js";
57
57
  export { pushContract } from "./contracts/push.contract.js";
58
58
  export { secretsContract } from "./contracts/secrets.contract.js";
59
59
  export { sessionsContract } from "./contracts/sessions.contract.js";
@@ -67,6 +67,7 @@ export * from "./effects.js";
67
67
  export * from "./events.js";
68
68
  export * from "./sse.js";
69
69
  export * from "./routes.js";
70
+ export * from "./workspace-state.js";
70
71
  export * from "./agent-catalog.js";
71
72
  export * from "./hostnames.js";
72
73
  export * from "./model-order.js";
@@ -94,7 +95,6 @@ export const sandboxContract = {
94
95
  settings: settingsContract,
95
96
  intentic: intenticContract,
96
97
  gemini: geminiContract,
97
- gate: gateContract,
98
98
  git: gitContract,
99
99
  grok: grokContract,
100
100
  kimi: kimiContract,
@@ -105,6 +105,7 @@ export const sandboxContract = {
105
105
  memory: memoryContract,
106
106
  panels: panelsContract,
107
107
  ports: portsContract,
108
+ prepush: prepushContract,
108
109
  push: pushContract,
109
110
  secrets: secretsContract,
110
111
  system: systemContract,
@@ -37,8 +37,10 @@ export interface QuickModelChoice {
37
37
  // for its entries (PickerEntry.key). Empty ⇒ Auto.
38
38
  export const quickModelKey = (choice: QuickModelChoice): string => `${choice.provider}:${choice.model}`;
39
39
 
40
- // Split on the FIRST colon only: a provider id never contains one and a model id might.
41
- const parsePinned = (pinned: string): QuickModelChoice | undefined => {
40
+ // Split on the FIRST colon only: a provider id never contains one and a model id might. Exported because the
41
+ // key shape is shared: `prepushFixModel` pins the suggested fix session's model the same way, and the dialog
42
+ // that seeds from it has to read one back.
43
+ export const parsePinned = (pinned: string): QuickModelChoice | undefined => {
42
44
  const separator = pinned.indexOf(`:`);
43
45
  if (separator <= 0 || separator === pinned.length - 1) {
44
46
  return undefined;
@@ -29,13 +29,12 @@ test("a payload from a build that predates a toggle parses, with the new toggle
29
29
  quickModel: "",
30
30
  agentRetentionDays: 3,
31
31
  autoLand: true,
32
- autoResumeOnLimit: false,
33
32
  resumeAfterOutage: true,
34
33
  autoResumeOnRestart: true,
35
- gateCommand: "",
36
- gateQuietMs: 20_000,
37
- gateTimeoutMs: 900_000,
38
- gateAutoFix: true,
34
+ prepushCommand: "",
35
+ prepushTimeoutMs: 900_000,
36
+ prepushFixModel: "",
37
+ prepushFixEffort: "",
39
38
  });
40
39
  });
41
40
 
@@ -68,19 +67,19 @@ test("an empty object is the full default settings object", () => {
68
67
  // On because it is the historical behaviour — defaulting off would silently hold every existing
69
68
  // sandbox's finished work on branches nobody is watching.
70
69
  autoLand: true,
71
- autoResumeOnLimit: false,
72
- // On, unlike the limit resume beside it: an outage resume spends nothing the dead turn hadn't already
73
- // committed, and the turns it saves are the unattended ones nobody is watching to restart by hand.
70
+ // On, where a spent usage limit re-runs nothing: an outage resume spends nothing the dead turn hadn't
71
+ // already committed, and the turns it saves are the unattended ones nobody is watching to restart by hand.
74
72
  resumeAfterOutage: true,
75
73
  // On: a daemon restart is usually intentic's own doing (an image update, an approved environment
76
74
  // change), not the user's decision, so the turn it interrupted resumes rather than staying stuck.
77
75
  autoResumeOnRestart: true,
78
- // Empty disables the landing gate until the owner supplies this workspace's verification command.
79
- gateCommand: "",
80
- gateQuietMs: 20_000,
81
- gateTimeoutMs: 900_000,
82
- // Once a gate is configured, a red verdict wakes one fixer by default.
83
- gateAutoFix: true,
76
+ // Empty disables the pre-push check until the owner supplies this workspace's verification command.
77
+ prepushCommand: "",
78
+ prepushTimeoutMs: 900_000,
79
+ // Empty ⇒ the suggested fix session opens on whatever the chat composer would have started with, which
80
+ // is the model the user already chose to work with.
81
+ prepushFixModel: "",
82
+ prepushFixEffort: "",
84
83
  });
85
84
  });
86
85
 
@@ -90,7 +89,3 @@ test("a key of the wrong type is still a parse failure — tolerance is for abse
90
89
  // The prompt cap is a real bound, not advice: the text IS the system prompt, and every turn pays for it.
91
90
  expect(SandboxSettingsSchema.safeParse({ systemPrompt: "x".repeat(20001) }).success).toBe(false);
92
91
  });
93
-
94
- test("usage-limit auto-resume stays off while the feature is disabled, including for an older saved true value", () => {
95
- expect(SandboxSettingsSchema.parse({ autoResumeOnLimit: true }).autoResumeOnLimit).toBe(false);
96
- });