@intentic/sandbox-contract 1.223.0 → 1.225.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 (165) hide show
  1. package/README.md +14 -14
  2. package/dist/agent-catalog.js +3 -3
  3. package/dist/agent-catalog.js.map +1 -1
  4. package/dist/chores/chores.js +43 -43
  5. package/dist/chores/chores.js.map +1 -1
  6. package/dist/chores/extension-update.js +2 -2
  7. package/dist/chores/extension-update.js.map +1 -1
  8. package/dist/chores/fix-deps.js +1 -1
  9. package/dist/chores/fix-deps.js.map +1 -1
  10. package/dist/chores/probes.js +1 -1
  11. package/dist/chores/probes.js.map +1 -1
  12. package/dist/chores/prompt.d.ts.map +1 -1
  13. package/dist/chores/prompt.js +3 -3
  14. package/dist/chores/prompt.js.map +1 -1
  15. package/dist/chores/verdict.js +2 -2
  16. package/dist/chores/verdict.js.map +1 -1
  17. package/dist/contracts/capabilities.contract.d.ts +41 -0
  18. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  19. package/dist/contracts/exit.contract.d.ts +80 -0
  20. package/dist/contracts/exit.contract.d.ts.map +1 -0
  21. package/dist/contracts/exit.contract.js +13 -0
  22. package/dist/contracts/exit.contract.js.map +1 -0
  23. package/dist/contracts/ports.contract.d.ts +11 -0
  24. package/dist/contracts/ports.contract.d.ts.map +1 -1
  25. package/dist/contracts/settings.contract.d.ts +12 -0
  26. package/dist/contracts/settings.contract.d.ts.map +1 -1
  27. package/dist/events.d.ts +4 -4
  28. package/dist/events.d.ts.map +1 -1
  29. package/dist/events.js +5 -5
  30. package/dist/events.js.map +1 -1
  31. package/dist/fast-tier.d.ts +9 -0
  32. package/dist/fast-tier.d.ts.map +1 -0
  33. package/dist/fast-tier.js +19 -0
  34. package/dist/fast-tier.js.map +1 -0
  35. package/dist/history-state.js +3 -3
  36. package/dist/history-state.js.map +1 -1
  37. package/dist/index.d.ts +214 -68
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +5 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/model-order.d.ts +1 -0
  42. package/dist/model-order.d.ts.map +1 -1
  43. package/dist/model-order.js +5 -0
  44. package/dist/model-order.js.map +1 -1
  45. package/dist/output-fields.d.ts.map +1 -1
  46. package/dist/output-fields.js +2 -2
  47. package/dist/output-fields.js.map +1 -1
  48. package/dist/prompt-complexity.d.ts +19 -0
  49. package/dist/prompt-complexity.d.ts.map +1 -0
  50. package/dist/prompt-complexity.js +83 -0
  51. package/dist/prompt-complexity.js.map +1 -0
  52. package/dist/publish-drafts.js +2 -2
  53. package/dist/publish-drafts.js.map +1 -1
  54. package/dist/schemas.d.ts +236 -0
  55. package/dist/schemas.d.ts.map +1 -1
  56. package/dist/schemas.js +116 -2
  57. package/dist/schemas.js.map +1 -1
  58. package/dist/workflow-faults.js +3 -3
  59. package/dist/workflow-faults.js.map +1 -1
  60. package/dist/workspace-state.d.ts +20 -20
  61. package/dist/workspace-state.d.ts.map +1 -1
  62. package/dist/workspace-state.js +20 -20
  63. package/dist/workspace-state.js.map +1 -1
  64. package/package.json +5 -5
  65. package/src/agent-catalog.test.ts +25 -25
  66. package/src/agent-catalog.ts +81 -81
  67. package/src/agent-run-model.test.ts +3 -3
  68. package/src/agent-run-model.ts +8 -8
  69. package/src/capability-env.ts +3 -3
  70. package/src/capability-ledger.test.ts +13 -13
  71. package/src/capability-secrets.ts +5 -5
  72. package/src/chores/chores.test.ts +1 -1
  73. package/src/chores/chores.ts +109 -109
  74. package/src/chores/digest.test.ts +1 -1
  75. package/src/chores/digest.ts +3 -3
  76. package/src/chores/extension-update.ts +5 -5
  77. package/src/chores/fix-deps.ts +4 -4
  78. package/src/chores/probes.test.ts +6 -6
  79. package/src/chores/probes.ts +17 -17
  80. package/src/chores/prompt.ts +9 -9
  81. package/src/chores/stack.test.ts +3 -3
  82. package/src/chores/stack.ts +17 -17
  83. package/src/chores/verdict.test.ts +20 -20
  84. package/src/chores/verdict.ts +25 -25
  85. package/src/contract-lock.test.ts +1 -1
  86. package/src/contract-lock.ts +7 -7
  87. package/src/contracts/activity.contract.ts +1 -1
  88. package/src/contracts/agent.contract.ts +2 -2
  89. package/src/contracts/agents.contract.ts +11 -11
  90. package/src/contracts/automations.contract.ts +5 -5
  91. package/src/contracts/capabilities.contract.ts +7 -7
  92. package/src/contracts/chores.contract.ts +3 -3
  93. package/src/contracts/ci.contract.ts +2 -2
  94. package/src/contracts/claude.contract.ts +3 -3
  95. package/src/contracts/drafts.contract.ts +1 -1
  96. package/src/contracts/endpoints.contract.ts +2 -2
  97. package/src/contracts/exit.contract.ts +42 -0
  98. package/src/contracts/extensions.contract.ts +8 -8
  99. package/src/contracts/git.contract.ts +6 -6
  100. package/src/contracts/grok.contract.ts +4 -4
  101. package/src/contracts/host.contract.ts +5 -5
  102. package/src/contracts/intentic.contract.ts +2 -2
  103. package/src/contracts/logs.contract.ts +1 -1
  104. package/src/contracts/loops.contract.ts +8 -8
  105. package/src/contracts/personas.contract.ts +8 -8
  106. package/src/contracts/ports.contract.ts +1 -1
  107. package/src/contracts/prepush.contract.ts +2 -2
  108. package/src/contracts/providers.contract.ts +4 -4
  109. package/src/contracts/public.contract.ts +1 -1
  110. package/src/contracts/push.contract.ts +2 -2
  111. package/src/contracts/secrets.contract.ts +2 -2
  112. package/src/contracts/sessions.contract.ts +2 -2
  113. package/src/contracts/settings.contract.ts +3 -3
  114. package/src/contracts/share.contract.ts +1 -1
  115. package/src/contracts/skills.contract.ts +2 -2
  116. package/src/contracts/system.contract.ts +9 -9
  117. package/src/contracts/usage.contract.ts +1 -1
  118. package/src/contracts/vpn.contract.ts +5 -5
  119. package/src/contracts/workflows.contract.ts +12 -12
  120. package/src/contracts/workspace.contract.ts +13 -13
  121. package/src/conversation-ids.ts +8 -8
  122. package/src/events.test.ts +3 -3
  123. package/src/events.ts +142 -142
  124. package/src/fast-tier.test.ts +88 -0
  125. package/src/fast-tier.ts +72 -0
  126. package/src/history-state.ts +15 -15
  127. package/src/host-protocol.ts +7 -7
  128. package/src/hostnames.test.ts +1 -1
  129. package/src/hostnames.ts +15 -15
  130. package/src/index.ts +13 -8
  131. package/src/listener-protocol.ts +12 -12
  132. package/src/model-order.test.ts +11 -11
  133. package/src/model-order.ts +52 -30
  134. package/src/output-fields.ts +11 -11
  135. package/src/path-refs.test.ts +4 -4
  136. package/src/path-refs.ts +6 -6
  137. package/src/prompt-complexity.test.ts +160 -0
  138. package/src/prompt-complexity.ts +271 -0
  139. package/src/publish-drafts.ts +8 -8
  140. package/src/quick-model.test.ts +11 -11
  141. package/src/quick-model.ts +15 -15
  142. package/src/routes.test.ts +11 -5
  143. package/src/routes.ts +19 -19
  144. package/src/runtime-state.test.ts +1 -1
  145. package/src/runtime-state.ts +22 -22
  146. package/src/schemas.test.ts +8 -8
  147. package/src/schemas.ts +1238 -921
  148. package/src/search-globs.test.ts +2 -2
  149. package/src/search-globs.ts +6 -6
  150. package/src/session-names.ts +5 -5
  151. package/src/share-paths.test.ts +1 -1
  152. package/src/share-paths.ts +7 -7
  153. package/src/sse.ts +1 -1
  154. package/src/state-portability.ts +8 -8
  155. package/src/terminal-protocol.ts +3 -3
  156. package/src/title.test.ts +9 -9
  157. package/src/title.ts +21 -21
  158. package/src/tunnel-ids.test.ts +3 -3
  159. package/src/tunnel-ids.ts +7 -7
  160. package/src/versions.test.ts +3 -3
  161. package/src/versions.ts +6 -6
  162. package/src/workflow-faults.test.ts +6 -6
  163. package/src/workflow-faults.ts +14 -14
  164. package/src/workspace-state.test.ts +32 -32
  165. package/src/workspace-state.ts +139 -139
@@ -0,0 +1,88 @@
1
+ import { expect, test } from "vitest";
2
+ import { fastTierModel } from "./fast-tier.js";
3
+
4
+ /* WHERE A DOWNGRADED TURN ACTUALLY LANDS. The judge (prompt-complexity.ts) says a turn could be cheaper; this
5
+ * says whether there is anywhere cheaper to put it, on the provider it is already on.
6
+ *
7
+ * Two properties carry the whole feature, and everything below is one of them: a downgrade is only ever to a
8
+ * genuinely CHEAPER rung of the same catalog (so "cheaper" can never quietly become "older", or "the same model
9
+ * with less thinking"), and it never crosses PROVIDER (because that retires the conversation's session, which
10
+ * throws away the context that made the follow-up cheap to answer in the first place). */
11
+
12
+ const CLAUDE = [`claude-opus-5`, `claude-sonnet-5`, `claude-haiku-4-5-20251001`];
13
+ const GOOGLE = [`gemini-3-pro`, `gemini-3-flash`, `gemini-3-flash-lite`];
14
+
15
+ const fastFor = (model: string, over: { models?: readonly string[]; pinned?: readonly string[]; provider?: string } = {}) =>
16
+ fastTierModel({ provider: over.provider ?? `claude`, model, models: over.models ?? CLAUDE, pinned: over.pinned ?? [] });
17
+
18
+ // --- Auto: the cheapest rung the provider publishes ----------------------------------------------------
19
+
20
+ test("sends a frontier turn to the provider's own cheap rung", () => {
21
+ expect(fastFor(`claude-opus-5`)).toBe(`claude-haiku-4-5-20251001`);
22
+ });
23
+
24
+ test("reads the cheap end the same way the quick model does, on every vendor's vocabulary", () => {
25
+ expect(fastFor(`gemini-3-pro`, { provider: `gemini`, models: GOOGLE })).toBe(`gemini-3-flash-lite`);
26
+ });
27
+
28
+ // --- the ceiling: the user's own pick -------------------------------------------------------------------
29
+
30
+ test("a user already on the cheap rung has nowhere to be sent", () => {
31
+ // The common case worth being exact about rather than an edge case. Undefined means "run what they asked
32
+ // for", which is the honest answer and the one that costs nothing to be wrong about.
33
+ expect(fastFor(`claude-haiku-4-5-20251001`)).toBeUndefined();
34
+ });
35
+
36
+ test("never swaps a model for an older build of the same tier", () => {
37
+ // A downgrade has to be legible AS a downgrade. Last year's Sonnet under this year's Sonnet is not a
38
+ // saving the feature promised, it is a different turn wearing the user's own model name.
39
+ expect(fastFor(`claude-sonnet-5`, { models: [`claude-sonnet-5`, `claude-sonnet-4`] })).toBeUndefined();
40
+ });
41
+
42
+ test("never downgrades a model whose family this build does not recognise", () => {
43
+ // An id carrying no tier word is a provider's base line or a family nobody here has heard of, and betting
44
+ // a user's turn on the guess that an unknown id is the budget one is the wrong direction to be wrong in.
45
+ expect(fastFor(`claude-opus-5`, { models: [`some-new-thing`] })).toBeUndefined();
46
+ expect(fastFor(`some-new-thing`, { models: CLAUDE })).toBeUndefined();
47
+ });
48
+
49
+ test("an unloaded catalog resolves to no downgrade rather than to a guess", () => {
50
+ expect(fastFor(`claude-opus-5`, { models: [] })).toBeUndefined();
51
+ });
52
+
53
+ test("no pick yet means nothing to be cheaper than", () => {
54
+ expect(fastFor(``)).toBeUndefined();
55
+ });
56
+
57
+ // --- pins -----------------------------------------------------------------------------------------------
58
+
59
+ test("a pin on this provider wins over the catalog's own cheap end", () => {
60
+ expect(fastFor(`claude-opus-5`, { pinned: [`claude:claude-sonnet-5`] })).toBe(`claude-sonnet-5`);
61
+ });
62
+
63
+ test("takes a pinned id verbatim, so a model the static catalog has not caught up with is still pinnable", () => {
64
+ // The same call resolveQuickModels makes, and for the same reason: the picker offers a custom-id escape
65
+ // hatch, and second-guessing the id here would run a different model than the settings row names.
66
+ expect(fastFor(`claude-opus-5`, { pinned: [`claude:claude-haiku-9`] })).toBe(`claude-haiku-9`);
67
+ });
68
+
69
+ test("drops a pin naming another provider rather than crossing to it", () => {
70
+ // Switching provider retires the conversation's session (turnRequest.ts `resumes`). Starting the
71
+ // conversation over to save a fraction of a cent is not a saving, so the pin is ignored and Auto answers.
72
+ expect(fastFor(`claude-opus-5`, { pinned: [`gemini:gemini-3-flash-lite`] })).toBe(`claude-haiku-4-5-20251001`);
73
+ });
74
+
75
+ test("skips a pin that is not actually cheaper than what the user picked", () => {
76
+ // A pin is a preference about WHICH cheap rung, never a licence to swap a model for its equal or better.
77
+ expect(fastFor(`claude-sonnet-5`, { pinned: [`claude:claude-opus-5`] })).toBe(`claude-haiku-4-5-20251001`);
78
+ });
79
+
80
+ test("walks past an unusable pin to the next one that names this provider", () => {
81
+ const pinned = [`gemini:gemini-3-flash`, `claude:claude-opus-5`, `claude:claude-haiku-4-5`];
82
+
83
+ expect(fastFor(`claude-opus-5`, { pinned })).toBe(`claude-haiku-4-5`);
84
+ });
85
+
86
+ test("falls back to Auto when no pin survives, rather than to no downgrade at all", () => {
87
+ expect(fastFor(`claude-opus-5`, { pinned: [`nonsense`, `gemini:gemini-3-flash`] })).toBe(`claude-haiku-4-5-20251001`);
88
+ });
@@ -0,0 +1,72 @@
1
+ import { compareCheapestFirst, isCheaperRung } from "./model-order.js";
2
+ import { parsePinned } from "./quick-model.js";
3
+ import type { AgentProvider } from "./schemas.js";
4
+
5
+ /* WHICH MODEL A DOWNGRADED TURN ACTUALLY RUNS ON, the second half of automatic tier selection.
6
+ * prompt-complexity.ts decides a turn COULD be cheaper; this decides whether there is anything cheaper to put
7
+ * it on, and names it.
8
+ *
9
+ * THERE IS NO "STANDARD TIER" SETTING, and its absence is the design rather than an omission. The standard tier
10
+ * is whatever the user already picked for this conversation. So this mechanism can only ever route DOWN, from a
11
+ * model somebody chose to a cheaper rung of the same catalog, which means the worst case of a wrong answer is
12
+ * one turn's quality on a model the user can see and correct, never a bill they did not ask for. Every
13
+ * ambiguous case in this file therefore resolves to `undefined`, which the caller reads as "run what they
14
+ * asked for".
15
+ *
16
+ * THE PROVIDER IS NEVER CROSSED, and this is the constraint the rest of the app imposes rather than one this
17
+ * file would have chosen. A conversation's provider session is resumed only while the selection still matches
18
+ * the runtime and account that minted it (turnRequest.ts `resumes`, which compares provider, account and
19
+ * harness, and pointedly NOT model). A model swap inside one provider is free, and the session carries on. A
20
+ * provider swap RETIRES the session and cuts a new segment, which throws away the very context that made the
21
+ * follow-up cheap to answer. So a cross-provider pin here is dropped rather than honoured: saving a fraction of
22
+ * a cent by starting the conversation over is not a saving.
23
+ *
24
+ * THE ORDER IS A LADDER, for the reason quick-model.ts is one, but a shorter one: the caller spends the head
25
+ * and falls back to the user's own pick, rather than walking rungs. A downgrade that cannot be started is not
26
+ * worth a second attempt when the honest answer (their model) is sitting right there. */
27
+
28
+ export interface FastTierInput {
29
+ // The provider this turn is on. Both the pick and every candidate belong to it; see above.
30
+ readonly provider: AgentProvider;
31
+ // The model the user picked, i.e. the standard tier and the ceiling. Empty when the composer has not
32
+ // resolved one yet, which resolves to no downgrade: there is nothing to be cheaper THAN.
33
+ readonly model: string;
34
+ // That provider's catalog as the caller can see it. Empty is a real state (a catalog that has not loaded),
35
+ // and it resolves to no downgrade rather than to a guess.
36
+ readonly models: readonly string[];
37
+ // settings.autoFastModels: an ordered list of `${provider}:${modelId}` keys (quickModelKey), or empty for
38
+ // Auto. Entries naming another provider are dropped, not honoured.
39
+ readonly pinned: readonly string[];
40
+ }
41
+
42
+ /* THE CHEAPER MODEL TO RUN THIS TURN ON, or undefined for "there isn't one, use their pick".
43
+ *
44
+ * A PIN IS TAKEN VERBATIM against the catalog, the same call resolveQuickModels makes and for the same reason:
45
+ * the model picker offers a custom-id escape hatch for a model the static catalog has not caught up with, and
46
+ * second-guessing the id here would run a different model than the settings row names. It is still checked for
47
+ * being CHEAPER, because that is not a fact about the catalog, it is a fact about the id, and a pin that is not
48
+ * cheaper than the pick is not a downgrade at all.
49
+ *
50
+ * AUTO IS THE CHEAPEST ROW THE PROVIDER PUBLISHES, read through the same cheap-end order the quick model uses,
51
+ * so the two features cannot disagree about which rung is the cheap one. Derived, never stored: connect an
52
+ * account tomorrow and the answer improves by itself, exactly as quickModel's empty default does.
53
+ *
54
+ * NOTHING CHEAPER THAN THE PICK ⇒ UNDEFINED, and that is the common case worth being exact about rather than
55
+ * the edge case: a user already working on the cheap rung has nowhere to be sent, and a user on a model whose
56
+ * family this build does not recognize is not going to be downgraded on a guess (isCheaperRung). */
57
+ export const fastTierModel = (input: FastTierInput): string | undefined => {
58
+ if (input.model === "") {
59
+ return undefined;
60
+ }
61
+ const pinned = input.pinned
62
+ .flatMap((key) => {
63
+ const choice = parsePinned(key);
64
+ return choice === undefined || choice.provider !== input.provider ? [] : [choice.model];
65
+ })
66
+ .find((model) => isCheaperRung(model, input.model));
67
+ if (pinned !== undefined) {
68
+ return pinned;
69
+ }
70
+ const cheapest = input.models.toSorted(compareCheapestFirst)[0];
71
+ return cheapest !== undefined && isCheaperRung(cheapest, input.model) ? cheapest : undefined;
72
+ };
@@ -1,14 +1,14 @@
1
1
  import type { StateFile } from "./state-portability.js";
2
2
 
3
- /* WHAT LIVES ON /history the second half of the daemon's state, and the half nothing declared until an
3
+ /* WHAT LIVES ON /history, the second half of the daemon's state, and the half nothing declared until an
4
4
  * export had to reason about it.
5
5
  *
6
6
  * `WORKSPACE_STATE_FILES` covers `<workspace>/.intentic/`, which is where the manifests live. It is not where
7
- * the machinery lives. Every repo's REAL git dir is here (a repo's in-tree `.git` is a pointer file see
7
+ * the machinery lives. Every repo's REAL git dir is here (a repo's in-tree `.git` is a pointer file, see
8
8
  * git/repo-git-dirs.ts for the invariant that forces it), and so are the fleet registry, the turn journal, the
9
9
  * ledgers, the checkpoint scopes and the isolated agents' checkouts. A "workspace export" that took `/work`
10
- * alone would carry a tree of repos with dangling gitdir pointers every git command in the restored sandbox
11
- * answering `fatal: not a git repository` and an empty agent board.
10
+ * alone would carry a tree of repos with dangling gitdir pointers, every git command in the restored sandbox
11
+ * answering `fatal: not a git repository`, and an empty agent board.
12
12
  *
13
13
  * The two tables stay separate rather than becoming one keyed by volume, because they answer different
14
14
  * questions. A `.intentic` entry also declares which browser QUERY it makes stale, since the file watcher
@@ -17,7 +17,7 @@ import type { StateFile } from "./state-portability.js";
17
17
  * imported rather than duplicated.
18
18
  *
19
19
  * `history-state-coverage.test.ts` fails when a daemon store builds a `/history` path this list doesn't carry, in both
20
- * directions the same shape-recognizing guard that covers the workspace table.
20
+ * directions, the same shape-recognizing guard that covers the workspace table.
21
21
  */
22
22
 
23
23
  // Paths are historyRoot-relative, forward-slash, matched by PREFIX; a directory entry keeps its trailing slash
@@ -27,7 +27,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
27
27
 
28
28
  /* THE ONE THAT MAKES A BUNDLE A WORKSPACE. Every repo's real git dir, including the /work root's own
29
29
  * ("root"), keyed by URI-encoded repo id. Carrying the working tree without this hands the target files
30
- * whose `.git` points at a path that does not exist there which is not a degraded repo but a broken one,
30
+ * whose `.git` points at a path that does not exist there, which is not a degraded repo but a broken one,
31
31
  * and it takes the Changes review, the diff, land and every agent branch with it. The agent BRANCHES live
32
32
  * in here too, which is what lets the checkouts below be left out. */
33
33
  { path: "gits/", portability: "carry" },
@@ -38,7 +38,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
38
38
  { path: "agents.json", portability: "carry" },
39
39
  { path: "turns/", portability: "carry" },
40
40
  { path: "transcripts/", portability: "carry" },
41
- // What each message can be put back to a workspace checkpoint, or an isolated conversation's own commits.
41
+ // What each message can be put back to, a workspace checkpoint, or an isolated conversation's own commits.
42
42
  // Carried WITH the transcripts and the scopes above, because it is the join between them: without it a
43
43
  // restored conversation reads back whole and offers no way back into it, even though both the messages and
44
44
  // the states they name travelled.
@@ -48,7 +48,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
48
48
  * Carried, and it is the entry with the most to say for itself: the PAGES live in the workspace's outbox
49
49
  * (`public/`), so they travel with `/work` whatever this says. Leaving the index behind would restore a
50
50
  * sandbox that is still serving somebody's conversation on the internet with nothing in the app that knows
51
- * it no row, no link, and no way to stop sharing short of deleting files by hand. The index is what makes
51
+ * it, no row, no link, and no way to stop sharing short of deleting files by hand. The index is what makes
52
52
  * a published page withdrawable, so it goes wherever the pages go. */
53
53
  { path: "shares.json", portability: "carry" },
54
54
  { path: "activity.jsonl", portability: "carry" },
@@ -58,7 +58,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
58
58
  // Explicit first-time dependency setup requests. Carrying the worklist preserves the owner's decision when
59
59
  // an export interrupts the queue before its terminal starts; fulfilled entries remove themselves.
60
60
  { path: "dependency-requests.json", portability: "carry" },
61
- // The deploy engine's own ledgers a run's events and the check results the Pipelines view reads back.
61
+ // The deploy engine's own ledgers, a run's events and the check results the Pipelines view reads back.
62
62
  { path: "apply-events.ndjson", portability: "carry" },
63
63
  { path: "check-events/", portability: "carry" },
64
64
 
@@ -68,7 +68,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
68
68
  *
69
69
  * A conversation's worktree is a full checkout of the monorepo per agent (plus its overlay upper dir), and
70
70
  * there can be a hundred of them. None of it is unique: the branch it holds is in `gits/` above, and the
71
- * registry entry naming it travels in agents.json so an imported conversation arrives in exactly the
71
+ * registry entry naming it travels in agents.json, so an imported conversation arrives in exactly the
72
72
  * shape the system already has a name for. `attached()` reports its checkout as absent, the board renders
73
73
  * it, and the next turn's `ensure()` re-creates it from the recorded composition, which is the same path an
74
74
  * archived agent takes when it runs again. The boot sweep's `git worktree prune` clears the stale admin
@@ -84,7 +84,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
84
84
  { path: ".isolation-probe", portability: "derived" },
85
85
  /* The finished bundles themselves. `derived` is doing real work here rather than describing leftovers: an
86
86
  * export that carried the export directory would pack every previous bundle into the new one, and the next
87
- * export would pack THAT each one a multiple of the last. Living on this volume is the other half of the
87
+ * export would pack THAT, each one a multiple of the last. Living on this volume is the other half of the
88
88
  * same guard; under `/work` the file would also be watched, indexed by iq, and snapshotted into history. */
89
89
  { path: "exports/", portability: "derived" },
90
90
 
@@ -95,7 +95,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
95
95
  {
96
96
  path: "ssh-hosts/",
97
97
  portability: "secret",
98
- note: "Re-add each ssh host on the Capabilities view its key does not travel.",
98
+ note: "Re-add each ssh host on the Capabilities view, its key does not travel.",
99
99
  },
100
100
  // The cli-proxy's config, which holds the routed subscriptions' provider tokens.
101
101
  {
@@ -107,8 +107,8 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
107
107
  /* ---- identity: what binds this sandbox to its owner, its browsers and its host ---- */
108
108
 
109
109
  /* Signs every browser session cookie. Carrying it would let a bundle's holder mint sessions against the
110
- * target an export becomes a credential and the target minting its own costs exactly one sign-in. */
111
- { path: "session-secret", portability: "identity", note: "Sign in again the target signs its own sessions." },
110
+ * target, an export becomes a credential, and the target minting its own costs exactly one sign-in. */
111
+ { path: "session-secret", portability: "identity", note: "Sign in again, the target signs its own sessions." },
112
112
  {
113
113
  path: "browser-access-disabled",
114
114
  portability: "identity",
@@ -117,7 +117,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
117
117
  {
118
118
  path: "push.json",
119
119
  portability: "identity",
120
- note: "Re-enable notifications in the browsers you use a push subscription is bound to the sandbox that minted it.",
120
+ note: "Re-enable notifications in the browsers you use, a push subscription is bound to the sandbox that minted it.",
121
121
  },
122
122
  { path: "sync-enrollments.json", portability: "identity", note: "Re-pair desktop sync from the Sync tab." },
123
123
  { path: "sync-pair-consumed.json", portability: "identity" },
@@ -1,36 +1,36 @@
1
1
  import { z } from "zod";
2
2
 
3
- /* The handshake on /system/hosts/connect the ONE message that is not oRPC.
3
+ /* The handshake on /system/hosts/connect, the ONE message that is not oRPC.
4
4
  *
5
5
  * Everything a connected computer is asked lives in `hostContract` (contracts/host.contract.ts), spoken over
6
6
  * this socket by oRPC's websocket adapter: the machine hosts the server, the daemon holds the client. But a
7
7
  * socket has to prove whose it is before it can be given a typed client, and that proof cannot itself be an
8
- * oRPC call the daemon has nothing to call yet, and would be attaching a link to a stranger.
8
+ * oRPC call, the daemon has nothing to call yet, and would be attaching a link to a stranger.
9
9
  *
10
10
  * So the machine's first act is this frame, in plain JSON. The daemon verifies the token, learns which
11
11
  * capability the socket belongs to, and only then attaches the link; from that message on, every byte on the
12
12
  * wire is oRPC. Anything arriving before the link exists is either this frame or a closed socket. */
13
13
 
14
14
  // The MCP protocol revision the machine's tool server implements. Shared because the daemon answers the
15
- // handshake ITSELF when the machine is asleep (hosts/host.routes.ts) two spellings of this would mean an
15
+ // handshake ITSELF when the machine is asleep (hosts/host.routes.ts), two spellings of this would mean an
16
16
  // offline machine negotiating a different protocol than the same machine awake.
17
17
  export const MCP_PROTOCOL_VERSION = "2025-06-18";
18
18
 
19
19
  export const HostHelloSchema = z.object({
20
20
  type: z.literal("hello"),
21
- /* The machine's enrollment token in the FIRST FRAME, never in the URL. A WebSocket has no headers to put
21
+ /* The machine's enrollment token, in the FIRST FRAME, never in the URL. A WebSocket has no headers to put
22
22
  * it in, and the obvious `?token=` would write a durable key to somebody's laptop into Cloudflare's edge
23
23
  * logs, the connector's logs and every proxy in between (the reasoning that moved the browser's upgrades
24
- * onto one-shot tickets auth/ws-tickets.ts). A frame is body, not URL, so it is logged nowhere. Until this
24
+ * onto one-shot tickets, auth/ws-tickets.ts). A frame is body, not URL, so it is logged nowhere. Until this
25
25
  * arrives the socket is anonymous and short-lived: the daemon closes it in seconds if it never does. */
26
26
  token: z.string(),
27
- // The @intentic/host build the machine is running surfaced per machine so an old binary is visible rather
27
+ // The @intentic/host build the machine is running, surfaced per machine so an old binary is visible rather
28
28
  // than mysteriously missing a tool. What the machine IS (`describe`) is not here: it is pulled over the
29
29
  // typed link a moment later, so there is one definition of those facts rather than two.
30
30
  version: z.string(),
31
31
  });
32
32
  export type HostHello = z.infer<typeof HostHelloSchema>;
33
33
 
34
- // The URL the machine's agent dials, given the sandbox's public URL. Carries no credential the token rides
34
+ // The URL the machine's agent dials, given the sandbox's public URL. Carries no credential, the token rides
35
35
  // the hello frame. One place builds it, so the agent and the daemon route can't disagree about where it lives.
36
36
  export const hostConnectUrl = (sandboxUrl: string): string => `${sandboxUrl.replace(/^http/, "ws").replace(/\/$/, "")}/system/hosts/connect`;
@@ -42,7 +42,7 @@ test("the local sync folder carries the very id the sandbox's URL does", () => {
42
42
  const url = `https://${sandboxHostname(ID, "intentic.dev")}`;
43
43
  expect(sandboxIdFromUrl(url)).toBe(ID);
44
44
  expect(syncFolder("Radarsu Local", url)).toBe(`~/intentic/radarsu-local-${ID}`);
45
- // A bare host (no scheme) and a port are both accepted the daemon/CLI and the browser pass different shapes.
45
+ // A bare host (no scheme) and a port are both accepted: the daemon/CLI and the browser pass different shapes.
46
46
  expect(sandboxIdFromUrl(`${sandboxHostname(ID, "intentic.dev")}:443`)).toBe(ID);
47
47
  // Own-Cloudflare: the owner's chosen subdomain IS the id, so the folder still matches what they browse to.
48
48
  expect(syncFolder("shop", "https://dev-box.example.com")).toBe("~/intentic/shop-dev-box");
package/src/hostnames.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // The single source for the tunnel/preview hostname + ingress scheme, shared by the CLI, platform API, daemon,
2
2
  // AND the browser. Pure string builders/parsers with NO node imports (unlike ./tunnel-ids, which needs
3
- // node:crypto for the digest) so the web bundle can import them and derive identical names. The caller supplies
3
+ // node:crypto for the digest), so the web bundle can import them and derive identical names. The caller supplies
4
4
  // the 12-hex id (sandboxIdFromToken/hostSshIdFromToken in node; WebCrypto in the browser).
5
5
  //
6
6
  // All four apps MUST agree on these strings: a divergence resolves to NXDOMAIN that resolvers negative-cache for
@@ -18,7 +18,7 @@ export const sshHostname = (id: string, zone: string): string => `ssh-${id}.${zo
18
18
  *
19
19
  * A public DNS name for a private address looks odd until you ask what the alternative is. A browser on the
20
20
  * same machine as the sandbox can reach its daemon in microseconds instead of crossing to a Cloudflare edge
21
- * and back but only over HTTPS, because Safari refuses http://127.0.0.1 from an HTTPS page as mixed content
21
+ * and back, but only over HTTPS, because Safari refuses http://127.0.0.1 from an HTTPS page as mixed content
22
22
  * (WebKit 171934, open since 2017), and HTTPS needs a name a public CA will certify. An IP literal cannot have
23
23
  * one; this can. The daemon holds the key and gets the certificate by proving control of the zone over
24
24
  * DNS-01 (there is nothing on the public internet for a CA to connect to).
@@ -36,36 +36,36 @@ export const hostSshTunnelName = (id: string): string => `host-ssh-${id}`;
36
36
  // The proxied-CNAME target every tunnel points its DNS record at.
37
37
  export const cfargotunnelCname = (tunnelId: string): string => `${tunnelId}.cfargotunnel.com`;
38
38
 
39
- // The cloudflared ingress catch-all must be the LAST rule.
39
+ // The cloudflared ingress catch-all, must be the LAST rule.
40
40
  export const CATCH_ALL = { service: "http_status:404" } as const;
41
41
 
42
- // Preview scheme: `preview-<panel>-<sandboxId>.<zone>` one DNS label (the free Universal SSL `*.<zone>` cert
42
+ // Preview scheme: `preview-<panel>-<sandboxId>.<zone>`, one DNS label (the free Universal SSL `*.<zone>` cert
43
43
  // covers exactly one level), where <panel> is `<repo>` or `<repo>--<app>` and <sandboxId> pins the hostname to
44
44
  // this sandbox (the shared intentic zone hosts many sandboxes; without the id two users' panels would collide).
45
- // Port-forward scheme: `port-<slot>-<sandboxId>.<zone>` the same shape with a `port-` prefix, where <slot>
45
+ // Port-forward scheme: `port-<slot>-<sandboxId>.<zone>`, the same shape with a `port-` prefix, where <slot>
46
46
  // is one of the sandbox's forward slots (portSlotsFromToken in ./tunnel-ids), not the port number itself:
47
47
  // slots keep the intentic-provided path's minted routes bounded and warm while dev servers churn ephemeral
48
48
  // ports. The slot labels are salted with the connect token rather than being the letters a…h, so a forwarded
49
- // port's hostname is not derivable from the (public) sandbox id alone see tunnel-ids for why that matters.
49
+ // port's hostname is not derivable from the (public) sandbox id alone, see tunnel-ids for why that matters.
50
50
  //
51
- // Outbox scheme: `public-<slot>-<sandboxId>.<zone>` the same shape again, serving the workspace's `public/`
51
+ // Outbox scheme: `public-<slot>-<sandboxId>.<zone>`, the same shape again, serving the workspace's `public/`
52
52
  // directory as static files. <slot> is publicSlotFromToken (./tunnel-ids), salted for the same reason the port
53
53
  // slots are, and one record per sandbox rather than a pool: there is one outbox, and its link has to stay good
54
54
  // for as long as the file does.
55
55
  //
56
56
  // A *label* is the first-DNS-label prefix before `-<sandboxId>` (`preview-<panel>` / `port-<slot>` /
57
- // `public-<slot>`) the unit the platform's /sandbox/preview-route mints, so one endpoint serves all three.
57
+ // `public-<slot>`), the unit the platform's /sandbox/preview-route mints, so one endpoint serves all three.
58
58
  export const previewLabel = (panel: string): string => `preview-${panel}`;
59
59
  export const portLabel = (slot: string): string => `port-${slot}`;
60
60
  export const publicLabel = (slot: string): string => `public-${slot}`;
61
61
 
62
- // The hostname a label resolves to what the platform's /sandbox/preview-route mints from the label alone.
62
+ // The hostname a label resolves to, what the platform's /sandbox/preview-route mints from the label alone.
63
63
  export const labelHostname = (label: string, id: string, zone: string): string => `${label}-${id}.${zone}`;
64
64
  export const previewHostname = (panel: string, id: string, zone: string): string => labelHostname(previewLabel(panel), id, zone);
65
65
  export const portHostname = (slot: string, id: string, zone: string): string => labelHostname(portLabel(slot), id, zone);
66
66
  export const publicHostname = (slot: string, id: string, zone: string): string => labelHostname(publicLabel(slot), id, zone);
67
67
 
68
- // A label's public URL undefined unless the sandbox has both a zone and an id (headless/loopback sandboxes
68
+ // A label's public URL, undefined unless the sandbox has both a zone and an id (headless/loopback sandboxes
69
69
  // have neither and advertise nothing). One builder, three vocabularies: a panel's preview, a forwarded port's,
70
70
  // and the outbox's.
71
71
  const labelUrl = (label: string, zone: string | undefined, sandboxId: string | undefined): string | undefined =>
@@ -79,7 +79,7 @@ export const publicUrl = (slot: string, zone: string | undefined, sandboxId: str
79
79
 
80
80
  // The key after `<prefix>` from a request's Host header. The first DNS label must carry the prefix (the
81
81
  // own-Cloudflare wildcard also catches stray subdomains → undefined → the caller's 404) and, when the sandbox
82
- // has an id, the exact `-<sandboxId>` suffix a fixed-length match, so keys containing `-` stay unambiguous.
82
+ // has an id, the exact `-<sandboxId>` suffix, a fixed-length match, so keys containing `-` stay unambiguous.
83
83
  // Without an id the bare label is the key (loopback tests and provider-deployed workspaces, which front the
84
84
  // proxy themselves).
85
85
  const keyFromHost = (prefix: string, hostHeader: string | undefined, sandboxId: string | undefined): string | undefined => {
@@ -103,7 +103,7 @@ export const publicSlotFromHost = (hostHeader: string | undefined, sandboxId: st
103
103
  keyFromHost("public-", hostHeader, sandboxId);
104
104
 
105
105
  // The sandbox's identity AS THE USER SEES IT: the leading DNS label of its public URL, minus the `sandbox-`
106
- // prefix `https://sandbox-0f310c3c4db4.intentic.dev` → `0f310c3c4db4`, i.e. sandboxIdFromToken's digest read
106
+ // prefix, `https://sandbox-0f310c3c4db4.intentic.dev` → `0f310c3c4db4`, i.e. sandboxIdFromToken's digest read
107
107
  // back off the wire by anyone holding only the URL. On the own-Cloudflare path the label is whatever subdomain
108
108
  // the owner chose, so that is the id there. undefined until the sandbox has a URL at all.
109
109
  export const sandboxIdFromUrl = (url: string | undefined): string | undefined => {
@@ -124,8 +124,8 @@ export const sandboxIdFromUrl = (url: string | undefined): string | undefined =>
124
124
  };
125
125
 
126
126
  // The LOCAL folder desktop sync mirrors /work into: `~/intentic/<sandbox name>-<sandboxIdFromUrl>`. Both halves
127
- // are strings the user already has in front of them the name in the sandbox switcher, the id in the address
128
- // bar so the folder on disk and the sandbox it mirrors read as ONE identity: `~/intentic/shop-0f310c3c4db4`
127
+ // are strings the user already has in front of them, the name in the sandbox switcher, the id in the address
128
+ // bar, so the folder on disk and the sandbox it mirrors read as ONE identity: `~/intentic/shop-0f310c3c4db4`
129
129
  // belongs to `https://sandbox-0f310c3c4db4.intentic.dev` and nothing else. Keyed on the URL rather than the
130
130
  // name alone for the same reason the hostname is: a torn-down sandbox recreated under the same name gets a new
131
131
  // id, hence its own fresh folder instead of reusing the dead one's (which cleanup never deletes) and colliding
@@ -142,7 +142,7 @@ export const syncFolder = (name: string, url: string | undefined): string => {
142
142
 
143
143
  // The Cloudflare zone from a sandbox public URL (https://sandbox-<id>.<zone> → <zone>): the hostname minus its
144
144
  // first DNS label. undefined when the URL is unparsable OR the hostname has fewer than three labels (no zone
145
- // suffix to strip e.g. a 2-label host would otherwise yield a bare TLD). Accepts scheme-less input too, so it
145
+ // suffix to strip, e.g. a 2-label host would otherwise yield a bare TLD). Accepts scheme-less input too, so it
146
146
  // works whether the caller passes `https://…` (daemon/CLI) or a bare host. This is the single reconciled
147
147
  // implementation of what used to be the daemon's `zoneFromPublicUrl` and the web's `zoneFromDaemonUrl`.
148
148
  export const zoneFromUrl = (url: string | undefined): string | undefined => {
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ import { ciContract } from "./contracts/ci.contract.js";
10
10
  import { claudeContract } from "./contracts/claude.contract.js";
11
11
  import { draftsContract } from "./contracts/drafts.contract.js";
12
12
  import { endpointsContract } from "./contracts/endpoints.contract.js";
13
+ import { exitContract } from "./contracts/exit.contract.js";
13
14
  import { extensionsContract } from "./contracts/extensions.contract.js";
14
15
  import { personasContract } from "./contracts/personas.contract.js";
15
16
  import { gitContract } from "./contracts/git.contract.js";
@@ -47,13 +48,14 @@ export { ciContract } from "./contracts/ci.contract.js";
47
48
  export { claudeContract } from "./contracts/claude.contract.js";
48
49
  export { draftsContract } from "./contracts/drafts.contract.js";
49
50
  export { endpointsContract, type TrialHealth, TrialStatusSchema, type TrialStatusResponse } from "./contracts/endpoints.contract.js";
51
+ export { exitContract } from "./contracts/exit.contract.js";
50
52
  export { extensionsContract } from "./contracts/extensions.contract.js";
51
53
  export { personasContract } from "./contracts/personas.contract.js";
52
54
  export { gitContract } from "./contracts/git.contract.js";
53
55
  export { grokContract } from "./contracts/grok.contract.js";
54
56
  export { historyContract } from "./contracts/history.contract.js";
55
57
  /* Deliberately NOT part of `sandboxContract` below: that map is the daemon's own HTTP surface, and this one is
56
- * spoken the other way round over a connected computer's WebSocket, with the MACHINE implementing it. */
58
+ * spoken the other way round, over a connected computer's WebSocket, with the MACHINE implementing it. */
57
59
  export { hostContract } from "./contracts/host.contract.js";
58
60
  export { intenticContract } from "./contracts/intentic.contract.js";
59
61
  export { inventoryContract } from "./contracts/inventory.contract.js";
@@ -82,13 +84,13 @@ export * from "./routes.js";
82
84
  /* THE CONTAINER'S FIXED DIRECTORY LAYOUT, re-exported so extensions can reach it.
83
85
  *
84
86
  * The names are defined once in @intentic/constants, which sits at the bottom of the dependency graph. An
85
- * EXTENSION may not import that package the boundary rule (.oxlintrc.json, _extensions/README.md) allows
87
+ * EXTENSION may not import that package, the boundary rule (.oxlintrc.json, _extensions/README.md) allows
86
88
  * only the SDK halves and this contract, so that an extension cannot couple itself to app or engine internals.
87
89
  * That rule is right, and it left extensions with no way to name the workspace root except by spelling it.
88
90
  *
89
91
  * Re-exporting here is what closes that gap without widening the boundary: the layout is exactly the kind of
90
- * thing this package already carries shared vocabulary both sides of the wire must agree on, alongside the
91
- * state-file table below and there is still one definition, in one place, that everything resolves to. */
92
+ * thing this package already carries, shared vocabulary both sides of the wire must agree on, alongside the
93
+ * state-file table below, and there is still one definition, in one place, that everything resolves to. */
92
94
  export { HISTORY_ROOT, HOST_STATE_ROOT, STATE_DIR, WORKSPACE_ROOT } from "@intentic/constants";
93
95
  export * from "./workspace-state.js";
94
96
  export * from "./runtime-state.js";
@@ -99,11 +101,13 @@ export * from "./agent-run-model.js";
99
101
  export * from "./capability-env.js";
100
102
  export * from "./capability-secrets.js";
101
103
  export * from "./conversation-ids.js";
104
+ export * from "./fast-tier.js";
102
105
  export * from "./host-protocol.js";
103
106
  export * from "./listener-protocol.js";
104
107
  export * from "./hostnames.js";
105
108
  export * from "./model-order.js";
106
109
  export * from "./path-refs.js";
110
+ export * from "./prompt-complexity.js";
107
111
  export * from "./quick-model.js";
108
112
  export * from "./output-fields.js";
109
113
  export * from "./publish-drafts.js";
@@ -114,7 +118,7 @@ export * from "./title.js";
114
118
  export * from "./versions.js";
115
119
  export * from "./workflow-faults.js";
116
120
 
117
- // The aggregated contract implemented on the server by the per-domain route factories and consumed by the
121
+ // The aggregated contract, implemented on the server by the per-domain route factories and consumed by the
118
122
  // browser's typed oRPC client (ContractRouterClient<typeof sandboxContract>). The wire paths it declares are
119
123
  // mounted at the sandbox root, so /health and /workspace/raw (plain Hono routes) sit alongside it.
120
124
  export const sandboxContract = {
@@ -153,18 +157,19 @@ export const sandboxContract = {
153
157
  translator: translatorContract,
154
158
  usage: usageContract,
155
159
  vpn: vpnContract,
160
+ exit: exitContract,
156
161
  workflows: workflowsContract,
157
162
  };
158
163
 
159
164
  // Every route in THIS build of the contract, and the names the daemon advertises on its hello frame. Bound here
160
- // rather than in routes.ts so that module stays a pure function of whatever contract it is handed importing
165
+ // rather than in routes.ts so that module stays a pure function of whatever contract it is handed, importing
161
166
  // `sandboxContract` from there would close a load-time cycle back through this file. See routes.ts for why a
162
167
  // daemon advertises its route surface at all.
163
168
  export const SANDBOX_ROUTES: readonly ContractRoute[] = contractRoutes(sandboxContract);
164
169
  export const SANDBOX_ROUTE_NAMES: readonly string[] = SANDBOX_ROUTES.map((route) => route.name);
165
170
 
166
171
  /* And the SHAPE of each of those routes, advertised beside the names for the failure the names cannot describe:
167
- * a route both builds have, answering a payload only one of them expects. Computed once at module load it
172
+ * a route both builds have, answering a payload only one of them expects. Computed once at module load, it
168
173
  * walks every contract schema through `z.toJSONSchema`, which is far too much work to repeat per connection and
169
174
  * exactly the kind of thing that never changes for the life of a process. See routes.ts. */
170
175
  export const SANDBOX_ROUTE_SHAPES: Readonly<Record<string, string>> = routeShapes(sandboxContract);
@@ -174,7 +179,7 @@ export const sandboxRouteName = (method: string, pathWithQuery: string): string
174
179
  routeNameForRequest(SANDBOX_ROUTES, method, pathWithQuery);
175
180
 
176
181
  // The method and concrete path a TYPED call is about to put on the wire, bound to this build's route table.
177
- // Undefined when the procedure is not one this contract declares, which a typed caller cannot reach the host
182
+ // Undefined when the procedure is not one this contract declares, which a typed caller cannot reach, the host
178
183
  // gate treats it as a refusal rather than assuming it is harmless.
179
184
  export const sandboxRequestFor = (procedure: readonly string[], input: unknown): { method: string; path: string } | undefined => {
180
185
  const route = routeForProcedure(SANDBOX_ROUTES, procedure);
@@ -3,14 +3,14 @@ import { ActivityStatusSchema } from "./schemas.js";
3
3
 
4
4
  /* The wire between the daemon and an extension's realtime-listener GATEWAY process (ext-discord, ext-slack,
5
5
  * ext-telegram, ext-whatsapp, ext-imap): the four provider-scoped routes app.ts mounts under
6
- * /listeners/:provider state, dispatch, failure, status. These shapes used to live daemon-side only, with
6
+ * /listeners/:provider, state, dispatch, failure, status. These shapes used to live daemon-side only, with
7
7
  * every gateway hand-writing its own copy of the payloads as untyped literals; a field rename broke five
8
- * producers silently. They live in the contract now so BOTH ends compile against one declaration the daemon
8
+ * producers silently. They live in the contract now so BOTH ends compile against one declaration, the daemon
9
9
  * parses with the schemas, the gateways (via @intentic/connector-runtime) type against the inferred types. */
10
10
 
11
- // One normalized inbound event serialized as a JSON line in the automation's payload, and the JSON body a
11
+ // One normalized inbound event, serialized as a JSON line in the automation's payload, and the JSON body a
12
12
  // realtime source POSTs to /listeners/<provider>/dispatch. A zod schema (not a bare interface) because it's
13
- // parsed from an extension gateway's request; `provider` and `type` are open strings the source is
13
+ // parsed from an extension gateway's request; `provider` and `type` are open strings, the source is
14
14
  // extension-declared (contributes.listener), not a core enum.
15
15
  export const ListenerMessageSchema = z.object({
16
16
  provider: z.string().min(1),
@@ -22,7 +22,7 @@ export const ListenerMessageSchema = z.object({
22
22
  // Discord message: it @mentions one of our bots or replies to a bot's message. Voice events never set it.
23
23
  mentioned: z.boolean().optional(),
24
24
  // CI pipeline event: the ref it ran on. Top-level rather than inside `extra` for the same reason
25
- // `mentioned` is the dispatcher MATCHES on it, and a narrowing axis the trigger can name has to be
25
+ // `mentioned` is, the dispatcher MATCHES on it, and a narrowing axis the trigger can name has to be
26
26
  // a field of the message rather than a key in a provider's opaque bag.
27
27
  branch: z.string().optional(),
28
28
  // Prior channel messages (chronological) fetched when a bot is tagged, so the agent can reason about why.
@@ -45,13 +45,13 @@ export const ListenerMessageSchema = z.object({
45
45
  });
46
46
  export type ListenerMessage = z.infer<typeof ListenerMessageSchema>;
47
47
 
48
- // One ndjson frame of a /listeners/<provider>/dispatch?stream=1 response a text delta for one automation's
48
+ // One ndjson frame of a /listeners/<provider>/dispatch?stream=1 response, a text delta for one automation's
49
49
  // reply, the provider's own failure sentence, or that automation's terminal marker. A type, not a schema: the
50
50
  // DAEMON produces these (listener.routes.ts), so nothing parses them from untrusted input.
51
51
  export interface ListenerDispatchFrame {
52
52
  readonly automationId: string;
53
53
  readonly delta?: string;
54
- // The turn refused or broke, in the provider's own words forwarded verbatim because a gateway delivers
54
+ // The turn refused or broke, in the provider's own words, forwarded verbatim because a gateway delivers
55
55
  // into the owner's own channel, where the actual sentence is the useful thing.
56
56
  readonly failed?: string;
57
57
  readonly end?: boolean;
@@ -61,16 +61,16 @@ export interface ListenerDispatchFrame {
61
61
  *
62
62
  * A CODE IS NOT THE ONLY THING WORTH SAYING, and publishing only codes is what made a phone that had never
63
63
  * linked read as connected: the seconds before the first code, a gateway that just restarted, and a number
64
- * WhatsApp refused all looked identical from the daemon's side an absent code so the card fell through to
64
+ * WhatsApp refused all looked identical from the daemon's side, an absent code, so the card fell through to
65
65
  * "ready" and the owner was sent away from the one screen that could have shown them the step. Each of those is
66
66
  * its own state here, and every one of them means NOT PAIRED YET.
67
67
  *
68
68
  * `since` stamps the CURRENT code. WhatsApp closes an unpaired socket after a minute or so and each reopen mints
69
- * a fresh code, so a code is a thing with an age the card says how old the one on screen is rather than
69
+ * a fresh code, so a code is a thing with an age, the card says how old the one on screen is rather than
70
70
  * leaving the owner to type a dead one twice. */
71
71
  export const ListenerPairingSchema = z.object({
72
72
  // waiting: a socket is up and the code hasn't arrived (or the last one died with its socket).
73
- // code: `code` is live type it on the phone. failed: `detail` says what WhatsApp refused.
73
+ // code: `code` is live, type it on the phone. failed: `detail` says what WhatsApp refused.
74
74
  state: z.enum(["waiting", "code", "failed"]),
75
75
  code: z.string().optional(),
76
76
  detail: z.string().optional(),
@@ -79,11 +79,11 @@ export const ListenerPairingSchema = z.object({
79
79
  export type ListenerPairing = z.infer<typeof ListenerPairingSchema>;
80
80
 
81
81
  // Push-based listener status: a gateway process POSTs its live connection/voice snapshot to
82
- // /listeners/<provider>/status, and the activity route reads it from there the daemon holds no provider
82
+ // /listeners/<provider>/status, and the activity route reads it from there, the daemon holds no provider
83
83
  // connection of its own to probe. The body IS the ActivityStatus the /activity/status probe used to build from
84
84
  // in-process discord singletons, plus the per-gateway extras that ride the same channel: whether whisper is
85
85
  // present (discord's voice-pending signal) and each unpaired capability's ceremony by id (whatsapp's
86
- // link-a-device flow the capability card renders it as the step the owner is standing in front of).
86
+ // link-a-device flow, the capability card renders it as the step the owner is standing in front of).
87
87
  export const ListenerStatusSchema = ActivityStatusSchema.extend({
88
88
  whisperReady: z.boolean().optional(),
89
89
  pairing: z.record(z.string(), ListenerPairingSchema).optional(),