@intentic/sandbox-contract 1.245.0 → 1.247.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.
- package/README.md +17 -1
- package/dist/batch-runs.d.ts +2 -0
- package/dist/batch-runs.d.ts.map +1 -1
- package/dist/batch-runs.js +1 -0
- package/dist/batch-runs.js.map +1 -1
- package/dist/command-classes.d.ts +6 -3
- package/dist/command-classes.d.ts.map +1 -1
- package/dist/command-classes.js +43 -18
- package/dist/command-classes.js.map +1 -1
- package/dist/contracts/{cursor.contract.d.ts → accounts.contract.d.ts} +102 -3
- package/dist/contracts/accounts.contract.d.ts.map +1 -0
- package/dist/contracts/accounts.contract.js +61 -0
- package/dist/contracts/accounts.contract.js.map +1 -0
- package/dist/contracts/agent.contract.d.ts +19 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +121 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +4 -4
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/ci.contract.d.ts +2 -0
- package/dist/contracts/ci.contract.d.ts.map +1 -1
- package/dist/contracts/host.contract.d.ts +35 -0
- package/dist/contracts/host.contract.d.ts.map +1 -1
- package/dist/contracts/host.contract.js +3 -2
- package/dist/contracts/host.contract.js.map +1 -1
- package/dist/contracts/personas.contract.d.ts +4 -2
- package/dist/contracts/personas.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +2 -2
- package/dist/contracts/settings.contract.d.ts +58 -20
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +80 -30
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.js +26 -17
- package/dist/contracts/system.contract.js.map +1 -1
- package/dist/contracts/usage.contract.d.ts +22 -0
- package/dist/contracts/usage.contract.d.ts.map +1 -1
- package/dist/contracts/usage.contract.js +19 -0
- package/dist/contracts/usage.contract.js.map +1 -1
- package/dist/definition.d.ts +20 -28
- package/dist/definition.d.ts.map +1 -1
- package/dist/documents.d.ts +0 -1
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.js +1 -2
- package/dist/documents.js.map +1 -1
- package/dist/embed.d.ts +23 -0
- package/dist/embed.d.ts.map +1 -0
- package/dist/embed.js +84 -0
- package/dist/embed.js.map +1 -0
- package/dist/events.d.ts +21 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +5 -2
- package/dist/events.js.map +1 -1
- package/dist/fast-tier.js +1 -1
- package/dist/fast-tier.js.map +1 -1
- package/dist/history-state.d.ts.map +1 -1
- package/dist/history-state.js +2 -0
- package/dist/history-state.js.map +1 -1
- package/dist/index.d.ts +453 -306
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -15
- package/dist/index.js.map +1 -1
- package/dist/model-pins.d.ts +17 -0
- package/dist/model-pins.d.ts.map +1 -0
- package/dist/{quick-model.js → model-pins.js} +18 -11
- package/dist/model-pins.js.map +1 -0
- package/dist/model-roles.d.ts +144 -0
- package/dist/model-roles.d.ts.map +1 -0
- package/dist/model-roles.js +129 -0
- package/dist/model-roles.js.map +1 -0
- package/dist/peer-dial.d.ts +33 -0
- package/dist/peer-dial.d.ts.map +1 -0
- package/dist/peer-dial.js +79 -0
- package/dist/peer-dial.js.map +1 -0
- package/dist/peer-mcp-server.d.ts +36 -0
- package/dist/peer-mcp-server.d.ts.map +1 -0
- package/dist/peer-mcp-server.js +71 -0
- package/dist/peer-mcp-server.js.map +1 -0
- package/dist/provider-specs.d.ts +38 -20
- package/dist/provider-specs.d.ts.map +1 -1
- package/dist/provider-specs.js +39 -13
- package/dist/provider-specs.js.map +1 -1
- package/dist/runtime-state.d.ts +1 -1
- package/dist/runtime-state.js +1 -1
- package/dist/runtime-state.js.map +1 -1
- package/dist/safety-policy.d.ts +12 -3
- package/dist/safety-policy.d.ts.map +1 -1
- package/dist/safety-policy.js +30 -5
- package/dist/safety-policy.js.map +1 -1
- package/dist/schemas/agent.d.ts +27 -8
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +10 -4
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents.d.ts +42 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +25 -4
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/automations.d.ts +11 -2
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/automations.js +1 -1
- package/dist/schemas/automations.js.map +1 -1
- package/dist/schemas/ci.d.ts +6 -0
- package/dist/schemas/ci.d.ts.map +1 -1
- package/dist/schemas/ci.js +3 -2
- package/dist/schemas/ci.js.map +1 -1
- package/dist/schemas/context.d.ts +30 -0
- package/dist/schemas/context.d.ts.map +1 -0
- package/dist/schemas/context.js +34 -0
- package/dist/schemas/context.js.map +1 -0
- package/dist/schemas/{computers.d.ts → devices.d.ts} +154 -60
- package/dist/schemas/devices.d.ts.map +1 -0
- package/dist/schemas/devices.js +157 -0
- package/dist/schemas/devices.js.map +1 -0
- package/dist/schemas/hosts.d.ts +12 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +1 -0
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/issues.d.ts +0 -5
- package/dist/schemas/issues.d.ts.map +1 -1
- package/dist/schemas/issues.js +0 -1
- package/dist/schemas/issues.js.map +1 -1
- package/dist/schemas/personas.d.ts +5 -3
- package/dist/schemas/personas.d.ts.map +1 -1
- package/dist/schemas/personas.js +3 -2
- package/dist/schemas/personas.js.map +1 -1
- package/dist/schemas/plan-limits.d.ts +20 -0
- package/dist/schemas/plan-limits.d.ts.map +1 -1
- package/dist/schemas/plan-limits.js +21 -0
- package/dist/schemas/plan-limits.js.map +1 -1
- package/dist/schemas/provider-oauth.d.ts +48 -16
- package/dist/schemas/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/provider-oauth.js +22 -20
- package/dist/schemas/provider-oauth.js.map +1 -1
- package/dist/schemas/settings.d.ts +42 -16
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +22 -28
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/terminal.js +9 -9
- package/dist/schemas/terminal.js.map +1 -1
- package/dist/schemas/usage.d.ts +5 -2
- package/dist/schemas/usage.d.ts.map +1 -1
- package/dist/schemas/usage.js +5 -2
- package/dist/schemas/usage.js.map +1 -1
- package/dist/shell-regions.d.ts +4 -0
- package/dist/shell-regions.d.ts.map +1 -0
- package/dist/shell-regions.js +156 -0
- package/dist/shell-regions.js.map +1 -0
- package/dist/workspace-state.d.ts +8 -0
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +13 -5
- package/dist/workspace-state.js.map +1 -1
- package/package.json +37 -4
- package/src/agent-catalog.ts +2 -2
- package/src/arrival.ts +3 -3
- package/src/batch-runs.test.ts +10 -5
- package/src/batch-runs.ts +10 -3
- package/src/command-classes.test.ts +195 -71
- package/src/command-classes.ts +148 -46
- package/src/contracts/accounts.contract.ts +94 -0
- package/src/contracts/agents.contract.ts +4 -3
- package/src/contracts/exit.contract.ts +2 -2
- package/src/contracts/host.contract.ts +17 -5
- package/src/contracts/settings.contract.ts +1 -1
- package/src/contracts/system.contract.ts +43 -24
- package/src/contracts/usage.contract.ts +31 -0
- package/src/contracts/vpn.contract.ts +2 -2
- package/src/documents.test.ts +2 -1
- package/src/documents.ts +7 -11
- package/src/embed.test.ts +68 -0
- package/src/embed.ts +164 -0
- package/src/events.ts +31 -4
- package/src/fast-tier.test.ts +1 -1
- package/src/fast-tier.ts +5 -5
- package/src/history-state.ts +12 -3
- package/src/host-protocol.ts +2 -2
- package/src/index.ts +8 -16
- package/src/model-order.ts +1 -1
- package/src/{quick-model.test.ts → model-pins.test.ts} +73 -29
- package/src/model-pins.ts +183 -0
- package/src/model-roles.ts +224 -0
- package/src/peer-dial.test.ts +203 -0
- package/src/peer-dial.ts +163 -0
- package/src/peer-mcp-server.test.ts +104 -0
- package/src/peer-mcp-server.ts +144 -0
- package/src/plan-pools.ts +1 -1
- package/src/prompt-complexity.test.ts +1 -1
- package/src/prompt-complexity.ts +2 -2
- package/src/provider-specs.test.ts +45 -18
- package/src/provider-specs.ts +147 -67
- package/src/routes.test.ts +6 -3
- package/src/runner-protocol.ts +1 -1
- package/src/runtime-state.ts +2 -2
- package/src/safety-policy.test.ts +88 -0
- package/src/safety-policy.ts +84 -14
- package/src/schemas/agent.ts +83 -30
- package/src/schemas/agents.ts +67 -6
- package/src/schemas/automations.ts +6 -4
- package/src/schemas/capabilities.ts +4 -4
- package/src/schemas/ci.ts +23 -6
- package/src/schemas/context.ts +87 -0
- package/src/schemas/{computers.ts → devices.ts} +190 -107
- package/src/schemas/hosts.ts +5 -1
- package/src/schemas/issues.ts +0 -4
- package/src/schemas/personas.ts +8 -3
- package/src/schemas/plan-limits.ts +50 -0
- package/src/schemas/provider-oauth.ts +49 -52
- package/src/schemas/settings.ts +105 -140
- package/src/schemas/terminal.ts +12 -12
- package/src/schemas/usage.ts +62 -27
- package/src/schemas/version-seam.test.ts +0 -1
- package/src/shell-regions.ts +289 -0
- package/src/versions.ts +2 -2
- package/src/webext-links.ts +2 -2
- package/src/webext-protocol.ts +2 -2
- package/src/workspace-state.test.ts +48 -1
- package/src/workspace-state.ts +48 -11
- package/dist/agent-run-model.d.ts +0 -4
- package/dist/agent-run-model.d.ts.map +0 -1
- package/dist/agent-run-model.js +0 -13
- package/dist/agent-run-model.js.map +0 -1
- package/dist/contracts/claude.contract.d.ts +0 -91
- package/dist/contracts/claude.contract.d.ts.map +0 -1
- package/dist/contracts/claude.contract.js +0 -50
- package/dist/contracts/claude.contract.js.map +0 -1
- package/dist/contracts/cursor.contract.d.ts.map +0 -1
- package/dist/contracts/cursor.contract.js +0 -50
- package/dist/contracts/cursor.contract.js.map +0 -1
- package/dist/contracts/grok.contract.d.ts +0 -36
- package/dist/contracts/grok.contract.d.ts.map +0 -1
- package/dist/contracts/grok.contract.js +0 -31
- package/dist/contracts/grok.contract.js.map +0 -1
- package/dist/contracts/keys.contract.d.ts +0 -81
- package/dist/contracts/keys.contract.d.ts.map +0 -1
- package/dist/contracts/keys.contract.js +0 -51
- package/dist/contracts/keys.contract.js.map +0 -1
- package/dist/quick-model.d.ts +0 -15
- package/dist/quick-model.d.ts.map +0 -1
- package/dist/quick-model.js.map +0 -1
- package/dist/schemas/computers.d.ts.map +0 -1
- package/dist/schemas/computers.js +0 -135
- package/dist/schemas/computers.js.map +0 -1
- package/src/agent-run-model.test.ts +0 -76
- package/src/agent-run-model.ts +0 -65
- package/src/contracts/claude.contract.ts +0 -71
- package/src/contracts/cursor.contract.ts +0 -74
- package/src/contracts/grok.contract.ts +0 -41
- package/src/contracts/keys.contract.ts +0 -79
- package/src/quick-model.ts +0 -155
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/* EVERY JOB IN THIS SANDBOX THAT PICKS A MODEL, LISTED BY THE JOB IT IS, one row per answer the owner is
|
|
4
|
+
* entitled to give differently.
|
|
5
|
+
*
|
|
6
|
+
* WHAT THIS REPLACED, and why it had to go. Model settings used to be grouped by how HARD the work was assumed
|
|
7
|
+
* to be: a "quick model" for the small automatic jobs, an "agent runs" tier for the big ones. Both names
|
|
8
|
+
* described an intensity rather than a job, and an intensity is a guess somebody else made about work the owner
|
|
9
|
+
* knows better. A commit message written by a frontier model is not a mistake, it is a preference, and the
|
|
10
|
+
* grouping made it unsayable: pinning Opus to get better commit subjects also pinned it to session titles, to
|
|
11
|
+
* every loop verdict, and to the safety judge. One bundled row could not say the thing anyone actually wanted
|
|
12
|
+
* to say.
|
|
13
|
+
*
|
|
14
|
+
* SO THE UNIT IS THE ROLE. Each entry here is one place a model gets chosen, and each gets its own ordered list
|
|
15
|
+
* in settings.modelRoles. Seventeen rows is more than four, and that is the point rather than a cost being
|
|
16
|
+
* absorbed: the configuration and the UI both already existed per use-case, and collapsing them was the only
|
|
17
|
+
* thing standing between the owner and a choice the machinery could already honour. A simpler face over this
|
|
18
|
+
* (presets: "cheap everywhere", "frontier everywhere") is a thing that can be built ON TOP of a true model, and
|
|
19
|
+
* cannot be unpicked from a lossy one.
|
|
20
|
+
*
|
|
21
|
+
* EVERY LIST IS AN ORDERED LADDER, whatever the role, and for one reason: the interesting failure of a pinned
|
|
22
|
+
* model is not that it is wrong, it is that it is CONNECTED AND WILL NOT ANSWER TODAY. The account's allowance
|
|
23
|
+
* went on the chat this morning, and one spent provider takes the role down for hours while three others sit
|
|
24
|
+
* idle. Written in order, the next entry catches it.
|
|
25
|
+
*
|
|
26
|
+
* TWO KINDS, and the only thing that separates them is what an EMPTY list means. That is a real fork rather
|
|
27
|
+
* than a leftover of the old grouping, and it is declared per role because it is a property of the job:
|
|
28
|
+
*
|
|
29
|
+
* helper — a one-shot. One prompt, no tools, one string back, and it is over. An empty list resolves to the
|
|
30
|
+
* AUTO LADDER: every connected provider's cheapest row, best-first (model-pins.ts). Deriving is the
|
|
31
|
+
* right default here because the job is small and repeatable, so the cost of a wrong guess is one
|
|
32
|
+
* cheap call, and because a derived answer improves by itself when an account is connected tomorrow.
|
|
33
|
+
*
|
|
34
|
+
* run — a whole session with tools and a worktree, started by a surface rather than by a person at a
|
|
35
|
+
* composer. An empty list resolves to NOTHING, and the caller's own floor answers: the model the
|
|
36
|
+
* owner picked for their chat. Nothing here can judge whether a job is worth the frontier tier, and
|
|
37
|
+
* a wrong guess is billed in whole sessions rather than in tokens, so the honest fallback is a
|
|
38
|
+
* choice they made rather than one this table invented.
|
|
39
|
+
*
|
|
40
|
+
* EVERY ENTRY IS A FULL PIN (ModelPinSchema): which model, and how it runs — effort, thinking, speed, harness.
|
|
41
|
+
* The helper roles carry them too, which they did not use to: a one-shot ran with reasoning forcibly off, so
|
|
42
|
+
* pinning a reasoning model to commit messages bought the price of one and the behaviour of neither. The knobs
|
|
43
|
+
* now ride through the one-shot path (the daemon's role-model.ts), so an entry means what it says wherever it
|
|
44
|
+
* is written.
|
|
45
|
+
*
|
|
46
|
+
* ADDING A ROLE IS ONE ROW HERE. The settings key, the resolver's floor, the daemon's lookup and the settings
|
|
47
|
+
* page's row all read this table, so a job that starts picking a model tomorrow becomes configurable by saying
|
|
48
|
+
* what it is. That is the property the old grouping cost: a new surface inherited "agent runs" by being
|
|
49
|
+
* unattended, which is how a documentation sweep and a production incident came to share one tier. */
|
|
50
|
+
|
|
51
|
+
export const ModelRoleKindSchema = z.enum(["helper", "run"]);
|
|
52
|
+
export type ModelRoleKind = z.infer<typeof ModelRoleKindSchema>;
|
|
53
|
+
|
|
54
|
+
/* The shape of a row. `id` is a bare string HERE and narrowed on the exported type below, because the id union
|
|
55
|
+
* is derived from this very table: a self-referential `satisfies` would be a type that has to know its own
|
|
56
|
+
* answer before it can check it. */
|
|
57
|
+
interface ModelRoleRow {
|
|
58
|
+
readonly id: string;
|
|
59
|
+
// What the settings row is called. A JOB, in the owner's words, never a tier.
|
|
60
|
+
readonly label: string;
|
|
61
|
+
// The row's one line: what this model is asked to do. Read beside the label, so it says what the label
|
|
62
|
+
// cannot rather than restating it.
|
|
63
|
+
readonly blurb: string;
|
|
64
|
+
readonly kind: ModelRoleKind;
|
|
65
|
+
// The row's glyph, from the shared icon set. Here rather than in a web-side map because the whole value of
|
|
66
|
+
// this table is that a role is declared ONCE; a second table keyed by the same ids is the drift this
|
|
67
|
+
// replaced, moved one layer up.
|
|
68
|
+
readonly icon: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* THE TABLE. Ordered as the settings page draws it, and the order is an argument about reach: the one-shots
|
|
72
|
+
* first, because nobody chose a model for them and they run constantly; then the runs somebody's click starts;
|
|
73
|
+
* then the runs that start themselves, which are the ones an owner is least likely to be watching and most
|
|
74
|
+
* likely to want held to a budget.
|
|
75
|
+
*
|
|
76
|
+
* The ids are the wire vocabulary: a turn carries one (AgentTurn.runRole), so they are kebab-case and stable,
|
|
77
|
+
* and renaming one is a breaking change to the setting rather than a cosmetic edit. */
|
|
78
|
+
export const MODEL_ROLES = [
|
|
79
|
+
{
|
|
80
|
+
id: "commit-message",
|
|
81
|
+
label: "Commit messages",
|
|
82
|
+
blurb: "The subject written when an agent's work lands, and the release note under it.",
|
|
83
|
+
kind: "helper",
|
|
84
|
+
icon: "file-edit",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "session-title",
|
|
88
|
+
label: "Session titles",
|
|
89
|
+
blurb: "The name a conversation wears on the board, written a second into its first turn.",
|
|
90
|
+
kind: "helper",
|
|
91
|
+
icon: "pencil",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
/* THE ONE HELPER WHOSE INPUT IS ADVERSARIAL, and the reason the old bundling was worst here. Its prompt
|
|
95
|
+
* contains a command the agent is about to run, which may have arrived from a stranger's web page, and a
|
|
96
|
+
* small model can be talked round by it. Being wrong is expensive in both directions: a needless card
|
|
97
|
+
* teaches the owner to click through the next one. */
|
|
98
|
+
id: "safety-judge",
|
|
99
|
+
label: "Safety judge",
|
|
100
|
+
blurb: "Which model reads your safety policy before a flagged command runs.",
|
|
101
|
+
kind: "helper",
|
|
102
|
+
icon: "shield",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "loop-verdict",
|
|
106
|
+
label: "Loop verdicts",
|
|
107
|
+
blurb: "Whether a loop's iteration met the goal, or the loop goes round again.",
|
|
108
|
+
kind: "helper",
|
|
109
|
+
icon: "check-square",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "pipeline-fix",
|
|
113
|
+
label: "Pipeline fixes",
|
|
114
|
+
blurb: "The agent started by Fix on a red pipeline.",
|
|
115
|
+
kind: "run",
|
|
116
|
+
icon: "wave-pulse",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "deployment-fix",
|
|
120
|
+
label: "Deployment fixes",
|
|
121
|
+
blurb: "The agent started by Fix on a deployment that is down.",
|
|
122
|
+
kind: "run",
|
|
123
|
+
icon: "server",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "maintenance-chore",
|
|
127
|
+
label: "Maintenance chores",
|
|
128
|
+
blurb: "A chore run started from the Maintenance board.",
|
|
129
|
+
kind: "run",
|
|
130
|
+
icon: "wrench",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: "documentation-run",
|
|
134
|
+
label: "Documentation runs",
|
|
135
|
+
blurb: "A pass over a repo's own documentation.",
|
|
136
|
+
kind: "run",
|
|
137
|
+
icon: "book",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "acceptance-run",
|
|
141
|
+
label: "Acceptance runs",
|
|
142
|
+
blurb: "One session per story in an acceptance fan-out.",
|
|
143
|
+
kind: "run",
|
|
144
|
+
icon: "list-check",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "pre-push-fix",
|
|
148
|
+
label: "Pre-push fixes",
|
|
149
|
+
blurb: "The fix proposed when a check fails on the way to a push.",
|
|
150
|
+
kind: "run",
|
|
151
|
+
icon: "cloud-upload",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "automation-wake",
|
|
155
|
+
label: "Automation wakes",
|
|
156
|
+
blurb: "A turn an automation fires: a schedule, a webhook, a message from outside.",
|
|
157
|
+
kind: "run",
|
|
158
|
+
icon: "clock",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "approval-queue",
|
|
162
|
+
label: "Approvals queue",
|
|
163
|
+
blurb: "The turn that publishes or acts on what you approved.",
|
|
164
|
+
kind: "run",
|
|
165
|
+
icon: "check-circle",
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "extension-review",
|
|
169
|
+
label: "Extension update reviews",
|
|
170
|
+
blurb: "The agent that reads an extension update before it is applied.",
|
|
171
|
+
kind: "run",
|
|
172
|
+
icon: "box",
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "loop-iteration",
|
|
176
|
+
label: "Loop iterations",
|
|
177
|
+
blurb: "Each round of a loop working towards its goal.",
|
|
178
|
+
kind: "run",
|
|
179
|
+
icon: "repeat",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: "watch-wake",
|
|
183
|
+
label: "Watch wakes",
|
|
184
|
+
blurb: "The turn a watch starts when the thing it was watching happens.",
|
|
185
|
+
kind: "run",
|
|
186
|
+
icon: "eye",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: "verify-nudge",
|
|
190
|
+
label: "Verify nudges",
|
|
191
|
+
blurb: "The follow-up turn sent when work was left unverified.",
|
|
192
|
+
kind: "run",
|
|
193
|
+
icon: "search",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
/* THE ONE ROLE THAT ANSWERS A CHOICE RATHER THAN FILLING A SILENCE. A spawning agent may name its
|
|
197
|
+
* child's provider, and when it does that wins, exactly as a caret pick wins on every other run role.
|
|
198
|
+
* This is what answers when it names none, which used to be a hardcoded "claude". */
|
|
199
|
+
id: "child-agent",
|
|
200
|
+
label: "Child agents",
|
|
201
|
+
blurb: "What an agent's own subagents run on when it names no model for them.",
|
|
202
|
+
kind: "run",
|
|
203
|
+
icon: "users",
|
|
204
|
+
},
|
|
205
|
+
] as const satisfies readonly ModelRoleRow[];
|
|
206
|
+
|
|
207
|
+
export type ModelRole = (typeof MODEL_ROLES)[number]["id"];
|
|
208
|
+
export type ModelRoleSpec = ModelRoleRow & { readonly id: ModelRole };
|
|
209
|
+
|
|
210
|
+
export const MODEL_ROLE_IDS = MODEL_ROLES.map((role) => role.id) as readonly ModelRole[];
|
|
211
|
+
|
|
212
|
+
/* The wire form. An enum rather than a string, unlike most ids in this contract, because there is no case for
|
|
213
|
+
* an unknown one: a role is a place in THIS codebase where a model gets chosen, so a value outside the table
|
|
214
|
+
* names nothing, and a settings file or a turn carrying one is a typo worth a clean error rather than a list
|
|
215
|
+
* silently ignored. */
|
|
216
|
+
export const ModelRoleSchema = z.enum(MODEL_ROLE_IDS as [ModelRole, ...ModelRole[]]);
|
|
217
|
+
|
|
218
|
+
const BY_ID = new Map<string, ModelRoleSpec>(MODEL_ROLES.map((role) => [role.id, role]));
|
|
219
|
+
|
|
220
|
+
/** What this role is, or undefined for an id no build of this table declares. */
|
|
221
|
+
export const modelRole = (id: string): ModelRoleSpec | undefined => BY_ID.get(id);
|
|
222
|
+
|
|
223
|
+
/** The roles of one kind, in table order: the two blocks the settings page draws. */
|
|
224
|
+
export const modelRolesOfKind = (kind: ModelRoleKind): readonly ModelRoleSpec[] => MODEL_ROLES.filter((role) => role.kind === kind);
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { expect, test, vi } from "vitest";
|
|
2
|
+
import { dialPeer, type SocketLike } from "./peer-dial.js";
|
|
3
|
+
|
|
4
|
+
/* The loop every peer runs, over a socket the test plays. What is at stake is the order of the two phases
|
|
5
|
+
* (handler attached BEFORE the hello goes out), the retry after a drop, the one close that is never retried,
|
|
6
|
+
* and that a stop reaches a dial still deciding where to go. */
|
|
7
|
+
|
|
8
|
+
class FakeSocket implements SocketLike {
|
|
9
|
+
readyState = 0;
|
|
10
|
+
readonly sent: string[] = [];
|
|
11
|
+
closed: { readonly code: number | undefined; readonly reason: string | undefined } | undefined;
|
|
12
|
+
private readonly listeners = new Map<string, Set<(event: { readonly code?: number }) => void>>();
|
|
13
|
+
|
|
14
|
+
addEventListener(type: "open" | "close" | "error", listener: (event: { readonly code?: number }) => void): void {
|
|
15
|
+
const set = this.listeners.get(type) ?? new Set();
|
|
16
|
+
set.add(listener);
|
|
17
|
+
this.listeners.set(type, set);
|
|
18
|
+
}
|
|
19
|
+
send(data: string): void {
|
|
20
|
+
this.sent.push(data);
|
|
21
|
+
}
|
|
22
|
+
close(code?: number, reason?: string): void {
|
|
23
|
+
this.closed = { code, reason };
|
|
24
|
+
this.readyState = 3;
|
|
25
|
+
}
|
|
26
|
+
opens(): void {
|
|
27
|
+
this.readyState = 1;
|
|
28
|
+
this.emit("open");
|
|
29
|
+
}
|
|
30
|
+
drops(code: number): void {
|
|
31
|
+
this.readyState = 3;
|
|
32
|
+
this.emit("close", code);
|
|
33
|
+
}
|
|
34
|
+
private emit(type: string, code?: number): void {
|
|
35
|
+
for (const listener of this.listeners.get(type) ?? []) {
|
|
36
|
+
listener(code === undefined ? {} : { code });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A ladder that answers with a fixed delay and records what it was told, so the test can read the held time.
|
|
42
|
+
const ladder = (delay = 1_000) => {
|
|
43
|
+
const held: number[] = [];
|
|
44
|
+
return {
|
|
45
|
+
held,
|
|
46
|
+
next: (heldMs: number) => {
|
|
47
|
+
held.push(heldMs);
|
|
48
|
+
return delay;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const dialling = () => {
|
|
54
|
+
const sockets: FakeSocket[] = [];
|
|
55
|
+
const attached: FakeSocket[] = [];
|
|
56
|
+
const said: string[] = [];
|
|
57
|
+
const revoked = vi.fn();
|
|
58
|
+
const link = dialPeer<FakeSocket>({
|
|
59
|
+
open: async () => {
|
|
60
|
+
const socket = new FakeSocket();
|
|
61
|
+
sockets.push(socket);
|
|
62
|
+
return { socket, said: `connected #${sockets.length}` };
|
|
63
|
+
},
|
|
64
|
+
hello: () => ({ type: "hello", token: "iht_test", version: "1.0.0" }),
|
|
65
|
+
attach: (socket) => void attached.push(socket),
|
|
66
|
+
backoff: ladder(),
|
|
67
|
+
log: (message) => void said.push(message),
|
|
68
|
+
revoked,
|
|
69
|
+
});
|
|
70
|
+
return { link, sockets, attached, said, revoked };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
test("the handler is attached before the hello goes out, and the hello is the first frame", async () => {
|
|
74
|
+
const { link, sockets, attached, said } = dialling();
|
|
75
|
+
await vi.waitFor(() => expect(sockets).toHaveLength(1));
|
|
76
|
+
expect(link.state()).toBe("connecting");
|
|
77
|
+
sockets[0]?.opens();
|
|
78
|
+
await vi.waitFor(() => expect(sockets[0]?.sent).toHaveLength(1));
|
|
79
|
+
expect(attached).toEqual([sockets[0]]);
|
|
80
|
+
expect(JSON.parse(sockets[0]?.sent[0] ?? "{}")).toEqual({ type: "hello", token: "iht_test", version: "1.0.0" });
|
|
81
|
+
expect(said).toEqual(["connected #1"]);
|
|
82
|
+
expect(link.state()).toBe("open");
|
|
83
|
+
link.stop();
|
|
84
|
+
await link.done;
|
|
85
|
+
expect(sockets[0]?.closed).toEqual({ code: 1000, reason: "stopping" });
|
|
86
|
+
expect(link.state()).toBe("closed");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a drop redials on the ladder, telling it how long the socket held", async () => {
|
|
90
|
+
vi.useFakeTimers();
|
|
91
|
+
try {
|
|
92
|
+
const { link, sockets, said } = dialling();
|
|
93
|
+
await vi.waitFor(() => expect(sockets).toHaveLength(1));
|
|
94
|
+
sockets[0]?.opens();
|
|
95
|
+
await vi.advanceTimersByTimeAsync(5_000);
|
|
96
|
+
sockets[0]?.drops(1006);
|
|
97
|
+
expect(link.state()).toBe("connecting");
|
|
98
|
+
expect(said.at(-1)).toBe("disconnected (1006); reconnecting in 1s");
|
|
99
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
100
|
+
await vi.waitFor(() => expect(sockets).toHaveLength(2));
|
|
101
|
+
link.stop();
|
|
102
|
+
await link.done;
|
|
103
|
+
} finally {
|
|
104
|
+
vi.useRealTimers();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("a refused enrollment (1008) is never retried and is reported once", async () => {
|
|
109
|
+
vi.useFakeTimers();
|
|
110
|
+
try {
|
|
111
|
+
const { link, sockets, revoked } = dialling();
|
|
112
|
+
await vi.waitFor(() => expect(sockets).toHaveLength(1));
|
|
113
|
+
sockets[0]?.opens();
|
|
114
|
+
sockets[0]?.drops(1008);
|
|
115
|
+
await link.done;
|
|
116
|
+
expect(revoked).toHaveBeenCalledTimes(1);
|
|
117
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
118
|
+
expect(sockets).toHaveLength(1);
|
|
119
|
+
expect(link.state()).toBe("closed");
|
|
120
|
+
} finally {
|
|
121
|
+
vi.useRealTimers();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// A stop that lands while the address is still being decided has nothing to close and must open nothing after
|
|
126
|
+
// the fact: a socket dialled by a loop that has already reported itself done would be a connection nobody stops.
|
|
127
|
+
test("a stop during resolution opens nothing, and a socket a slow resolver still hands over is closed", async () => {
|
|
128
|
+
let answer: (() => void) | undefined;
|
|
129
|
+
const sockets: FakeSocket[] = [];
|
|
130
|
+
const link = dialPeer<FakeSocket>({
|
|
131
|
+
open: async (signal) => {
|
|
132
|
+
await new Promise<void>((resolve) => {
|
|
133
|
+
answer = resolve;
|
|
134
|
+
});
|
|
135
|
+
if (signal.aborted) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
const socket = new FakeSocket();
|
|
139
|
+
sockets.push(socket);
|
|
140
|
+
return { socket };
|
|
141
|
+
},
|
|
142
|
+
hello: () => ({}),
|
|
143
|
+
attach: () => undefined,
|
|
144
|
+
backoff: ladder(),
|
|
145
|
+
log: () => undefined,
|
|
146
|
+
revoked: () => undefined,
|
|
147
|
+
});
|
|
148
|
+
await vi.waitFor(() => expect(answer).toEqual(expect.any(Function)));
|
|
149
|
+
link.stop();
|
|
150
|
+
await link.done;
|
|
151
|
+
answer?.();
|
|
152
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
153
|
+
expect(sockets).toHaveLength(0);
|
|
154
|
+
|
|
155
|
+
// The other order: a resolver that ignores the signal and hands a socket over anyway.
|
|
156
|
+
const late: FakeSocket[] = [];
|
|
157
|
+
const careless = dialPeer<FakeSocket>({
|
|
158
|
+
open: async () => {
|
|
159
|
+
await new Promise<void>((resolve) => {
|
|
160
|
+
answer = resolve;
|
|
161
|
+
});
|
|
162
|
+
const socket = new FakeSocket();
|
|
163
|
+
late.push(socket);
|
|
164
|
+
return { socket };
|
|
165
|
+
},
|
|
166
|
+
hello: () => ({}),
|
|
167
|
+
attach: () => undefined,
|
|
168
|
+
backoff: ladder(),
|
|
169
|
+
log: () => undefined,
|
|
170
|
+
revoked: () => undefined,
|
|
171
|
+
});
|
|
172
|
+
await vi.waitFor(() => expect(answer).toEqual(expect.any(Function)));
|
|
173
|
+
careless.stop();
|
|
174
|
+
answer?.();
|
|
175
|
+
await vi.waitFor(() => expect(late[0]?.closed).toEqual({ code: 1000, reason: "stopping" }));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("a pairing that is gone by the next attempt ends the loop rather than dialling nowhere", async () => {
|
|
179
|
+
let pairings = 1;
|
|
180
|
+
const sockets: FakeSocket[] = [];
|
|
181
|
+
const link = dialPeer<FakeSocket>({
|
|
182
|
+
open: async () => {
|
|
183
|
+
if (pairings === 0) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
pairings -= 1;
|
|
187
|
+
const socket = new FakeSocket();
|
|
188
|
+
sockets.push(socket);
|
|
189
|
+
return { socket };
|
|
190
|
+
},
|
|
191
|
+
hello: () => ({}),
|
|
192
|
+
attach: () => undefined,
|
|
193
|
+
backoff: ladder(0),
|
|
194
|
+
log: () => undefined,
|
|
195
|
+
revoked: () => undefined,
|
|
196
|
+
});
|
|
197
|
+
await vi.waitFor(() => expect(sockets).toHaveLength(1));
|
|
198
|
+
sockets[0]?.opens();
|
|
199
|
+
sockets[0]?.drops(1006);
|
|
200
|
+
await link.done;
|
|
201
|
+
expect(sockets).toHaveLength(1);
|
|
202
|
+
expect(link.state()).toBe("closed");
|
|
203
|
+
});
|
package/src/peer-dial.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/* THE ONE SOCKET a peer holds to its sandbox, written once for the three things that hold one: the machine
|
|
2
|
+
* agent (@intentic/machine), the browser extension (@intentic/webext) and a runner (the daemon itself, in
|
|
3
|
+
* runner mode). Each dials OUT, sends a plain-JSON hello carrying its enrollment token (host-protocol.ts,
|
|
4
|
+
* webext-protocol.ts, runner-protocol.ts), and from then on everything the sandbox asks arrives on the socket
|
|
5
|
+
* as oRPC against the door's contract, with the PEER serving.
|
|
6
|
+
*
|
|
7
|
+
* OUTBOUND ONLY, and that is the entire networking story: no port is opened, no router configured, no VPN
|
|
8
|
+
* joined. A laptop on hotel wifi behind a corporate proxy holds this connection because it is an ordinary
|
|
9
|
+
* outbound wss://, the same thing every chat app on the machine is already doing.
|
|
10
|
+
*
|
|
11
|
+
* The socket has exactly two phases. First the hello, plain JSON, because the sandbox has nothing to call until
|
|
12
|
+
* it knows whose socket this is (the token rides the FRAME, never the URL, which would write a durable key into
|
|
13
|
+
* every proxy log between the peer and the sandbox). Then the sandbox attaches its client and the socket is pure
|
|
14
|
+
* oRPC: request/response correlation, argument validation and error shape all belong to the link from that
|
|
15
|
+
* point on, which is why there is no message plumbing here.
|
|
16
|
+
*
|
|
17
|
+
* THE HANDLER IS ATTACHED BEFORE THE HELLO GOES OUT: the sandbox may call the moment it has verified the token,
|
|
18
|
+
* and a race there would drop the first `setScopes`, the one call whose loss would leave a device enforcing a
|
|
19
|
+
* stale grant.
|
|
20
|
+
*
|
|
21
|
+
* RECONNECTION IS THE NORMAL CASE, not the failure case. Lids close, wifi changes, tunnels idle out, sandboxes
|
|
22
|
+
* restart on every rebuild. So a dropped socket is routine and comes back on an exponential ladder the caller
|
|
23
|
+
* supplies (@intentic/base's createBackoff, over PEER_LINK_BACKOFF): a link that held for a minute was working
|
|
24
|
+
* and its drop redials at the floor; one that opened and died at once keeps climbing. The one thing that is NOT
|
|
25
|
+
* retried is a refused enrollment (close code 1008): the owner revoked this peer, that never heals on its own,
|
|
26
|
+
* and hammering a door that has been locked is how an agent turns a revocation into a support ticket.
|
|
27
|
+
*
|
|
28
|
+
* Here, in the contract, beside the hello schemas it sends and the URL builders it dials, rather than in a
|
|
29
|
+
* package of its own: every peer already depends on this one, and the loop is the far end of the same wire. */
|
|
30
|
+
|
|
31
|
+
// Backoff bounds. The first retry is fast because the overwhelmingly common cause is a sandbox restart, which
|
|
32
|
+
// takes seconds; the ceiling is low enough that a laptop opened after a night asleep is back within a minute.
|
|
33
|
+
export const PEER_LINK_BACKOFF = { floorMs: 1_000, capMs: 30_000, stableMs: 60_000 } as const;
|
|
34
|
+
|
|
35
|
+
// The sandbox closes with this when the token is not enrolled: a decision, not a fault, and one that never heals.
|
|
36
|
+
const UNAUTHORIZED = 1008;
|
|
37
|
+
|
|
38
|
+
// A socket's readyState value for "open", per the WebSocket spec, named so a caller's fake need not import a class.
|
|
39
|
+
const OPEN = 1;
|
|
40
|
+
|
|
41
|
+
// What the two runtimes' sockets have in common, taken structurally: a browser's WebSocket, node's global one,
|
|
42
|
+
// and a test's fake all satisfy it without a cast.
|
|
43
|
+
export interface SocketLike {
|
|
44
|
+
readonly readyState: number;
|
|
45
|
+
addEventListener(type: "open" | "close" | "error", listener: (event: { readonly code?: number }) => void): void;
|
|
46
|
+
send(data: string): void;
|
|
47
|
+
close(code?: number, reason?: string): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PeerDialSpec<S extends SocketLike> {
|
|
51
|
+
/* ONE ATTEMPT'S SOCKET, dialled at whatever address the caller resolves for THIS attempt: a sandbox on the
|
|
52
|
+
* same machine is a loopback hop away on one attempt and behind its tunnel on the next, and asking on every
|
|
53
|
+
* attempt rather than once at startup is exactly what a reconnect is about. `said` is the line the log gets
|
|
54
|
+
* once the socket is open. Undefined ⇒ there is nothing to dial any more (the pairing was forgotten), and
|
|
55
|
+
* the loop ends. `signal` is aborted by `stop`, so a resolution that outlives the stop opens nothing. */
|
|
56
|
+
readonly open: (signal: AbortSignal) => Promise<{ readonly socket: S; readonly said?: string } | undefined>;
|
|
57
|
+
// The first frame, carrying the enrollment token and whatever the door's hello schema asks for.
|
|
58
|
+
readonly hello: () => Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
59
|
+
// Hand the open socket to the oRPC handler serving the door's contract.
|
|
60
|
+
readonly attach: (socket: S) => void;
|
|
61
|
+
// The retry ladder: how long to wait after a drop, given how long the socket had held.
|
|
62
|
+
readonly backoff: { readonly next: (heldMs: number) => number };
|
|
63
|
+
readonly log: (message: string) => void;
|
|
64
|
+
// The sandbox refused the enrollment (1008). The loop has already stopped; this is where the peer says so.
|
|
65
|
+
readonly revoked: () => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface PeerLink {
|
|
69
|
+
// Resolves when the loop is asked to stop or refused for good; never rejects, a connection error is a retry.
|
|
70
|
+
readonly done: Promise<void>;
|
|
71
|
+
readonly stop: (reason?: string) => void;
|
|
72
|
+
// "connecting" covers a dial in flight AND a retry waiting on the ladder: both mean nobody should start another.
|
|
73
|
+
readonly state: () => "open" | "connecting" | "closed";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const dialPeer = <S extends SocketLike>(spec: PeerDialSpec<S>): PeerLink => {
|
|
77
|
+
const stopping = new AbortController();
|
|
78
|
+
let socket: S | undefined;
|
|
79
|
+
let waiting = false;
|
|
80
|
+
let openedAt: number | undefined;
|
|
81
|
+
let resolveDone: () => void = () => undefined;
|
|
82
|
+
const done = new Promise<void>((resolve) => {
|
|
83
|
+
resolveDone = resolve;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const open = async (): Promise<void> => {
|
|
87
|
+
waiting = true;
|
|
88
|
+
const attempt = await spec.open(stopping.signal);
|
|
89
|
+
waiting = false;
|
|
90
|
+
// Stopped while the address was still being decided: there is nothing to open, and `stop` has already
|
|
91
|
+
// settled `done`. A socket a slow resolver still handed over is closed rather than left dialling.
|
|
92
|
+
if (stopping.signal.aborted) {
|
|
93
|
+
attempt?.socket.close(1000, "stopping");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (attempt === undefined) {
|
|
97
|
+
stopping.abort();
|
|
98
|
+
resolveDone();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const ws = attempt.socket;
|
|
102
|
+
socket = ws;
|
|
103
|
+
|
|
104
|
+
ws.addEventListener("open", () => {
|
|
105
|
+
openedAt = Date.now();
|
|
106
|
+
spec.attach(ws);
|
|
107
|
+
const send = (hello: Record<string, unknown>): void => {
|
|
108
|
+
ws.send(JSON.stringify(hello));
|
|
109
|
+
if (attempt.said !== undefined) {
|
|
110
|
+
spec.log(attempt.said);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
// Sent in the open handler's own tick where the hello is already in hand: the sandbox's auth
|
|
114
|
+
// deadline is generous, but a frame that could go now and waits a tick is a frame a test cannot
|
|
115
|
+
// read, and a hello that needs a read first (a runner's settings claim) simply follows it.
|
|
116
|
+
const hello = spec.hello();
|
|
117
|
+
if (hello instanceof Promise) {
|
|
118
|
+
void hello.then(send);
|
|
119
|
+
} else {
|
|
120
|
+
send(hello);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
ws.addEventListener("close", (event) => {
|
|
125
|
+
socket = undefined;
|
|
126
|
+
if (stopping.signal.aborted) {
|
|
127
|
+
resolveDone();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (event.code === UNAUTHORIZED) {
|
|
131
|
+
stopping.abort();
|
|
132
|
+
resolveDone();
|
|
133
|
+
spec.revoked();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const delay = spec.backoff.next(openedAt === undefined ? 0 : Date.now() - openedAt);
|
|
137
|
+
openedAt = undefined;
|
|
138
|
+
spec.log(`disconnected (${event.code ?? "no code"}); reconnecting in ${Math.round(delay / 1000)}s`);
|
|
139
|
+
waiting = true;
|
|
140
|
+
setTimeout(() => void open(), delay);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// A socket error is always followed by a close event, which owns the retry; this only records the cause,
|
|
144
|
+
// which the close code alone never carries.
|
|
145
|
+
ws.addEventListener("error", () => spec.log("connection error"));
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
void open();
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
done,
|
|
152
|
+
stop: (reason = "stopping") => {
|
|
153
|
+
stopping.abort();
|
|
154
|
+
waiting = false;
|
|
155
|
+
// Forgotten before the close frame arrives, so `state` reads closed the moment the peer asked.
|
|
156
|
+
const held = socket;
|
|
157
|
+
socket = undefined;
|
|
158
|
+
held?.close(1000, reason);
|
|
159
|
+
resolveDone();
|
|
160
|
+
},
|
|
161
|
+
state: () => (socket?.readyState === OPEN ? "open" : socket !== undefined || waiting ? "connecting" : "closed"),
|
|
162
|
+
};
|
|
163
|
+
};
|