@intentic/sandbox-contract 1.164.0 → 1.166.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/dist/contracts/agent.contract.d.ts +20 -2
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +3 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/automations.contract.d.ts +7 -0
- package/dist/contracts/automations.contract.d.ts.map +1 -1
- package/dist/contracts/automations.contract.js +1 -0
- package/dist/contracts/automations.contract.js.map +1 -1
- package/dist/contracts/ci.contract.d.ts +27 -0
- package/dist/contracts/ci.contract.d.ts.map +1 -1
- package/dist/contracts/ci.contract.js +3 -1
- package/dist/contracts/ci.contract.js.map +1 -1
- package/dist/contracts/extensions.contract.d.ts +1 -0
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/gate.contract.d.ts +47 -0
- package/dist/contracts/gate.contract.d.ts.map +1 -0
- package/dist/contracts/gate.contract.js +9 -0
- package/dist/contracts/gate.contract.js.map +1 -0
- package/dist/contracts/sessions.contract.d.ts +3 -0
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +10 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +48 -4
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +3 -1
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/events.d.ts +127 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +23 -2
- package/dist/events.js.map +1 -1
- package/dist/hostnames.d.ts +2 -0
- package/dist/hostnames.d.ts.map +1 -1
- package/dist/hostnames.js +2 -0
- package/dist/hostnames.js.map +1 -1
- package/dist/index.d.ts +165 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/model-order.d.ts.map +1 -1
- package/dist/model-order.js +25 -6
- package/dist/model-order.js.map +1 -1
- package/dist/schemas.d.ts +130 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +55 -3
- package/dist/schemas.js.map +1 -1
- package/dist/session-names.d.ts +8 -0
- package/dist/session-names.d.ts.map +1 -0
- package/dist/session-names.js +17 -0
- package/dist/session-names.js.map +1 -0
- package/package.json +13 -2
- package/src/contracts/automations.contract.ts +13 -0
- package/src/contracts/ci.contract.ts +6 -1
- package/src/contracts/gate.contract.ts +19 -0
- package/src/events.ts +66 -7
- package/src/hostnames.ts +16 -0
- package/src/index.ts +3 -0
- package/src/model-order.test.ts +29 -12
- package/src/model-order.ts +49 -17
- package/src/quick-model.test.ts +2 -2
- package/src/schemas.test.ts +14 -0
- package/src/schemas.ts +204 -10
- package/src/session-names.ts +44 -0
package/src/hostnames.ts
CHANGED
|
@@ -14,6 +14,22 @@ export const sandboxHostname = (id: string, zone: string): string => `${sandboxS
|
|
|
14
14
|
// The container sshd hostname the desktop-sync (Mutagen) reaches over the sandbox tunnel: `ssh-<id>.<zone>`.
|
|
15
15
|
export const sshHostname = (id: string, zone: string): string => `ssh-${id}.${zone}`;
|
|
16
16
|
|
|
17
|
+
/* The LOOPBACK name: `local-<id>.<zone>`, an A record pointing at 127.0.0.1.
|
|
18
|
+
*
|
|
19
|
+
* A public DNS name for a private address looks odd until you ask what the alternative is. A browser on the
|
|
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
|
|
22
|
+
* (WebKit 171934, open since 2017), and HTTPS needs a name a public CA will certify. An IP literal cannot have
|
|
23
|
+
* one; this can. The daemon holds the key and gets the certificate by proving control of the zone over
|
|
24
|
+
* DNS-01 (there is nothing on the public internet for a CA to connect to).
|
|
25
|
+
*
|
|
26
|
+
* It discloses nothing: the id is already the leading label of the sandbox's public hostname, and the address
|
|
27
|
+
* it resolves to is every machine's own loopback. */
|
|
28
|
+
export const localHostname = (id: string, zone: string): string => `local-${id}.${zone}`;
|
|
29
|
+
|
|
30
|
+
// What that record points at, and the reason it is safe to publish: every resolver on earth gets 127.0.0.1.
|
|
31
|
+
export const LOCAL_ADDRESS = "127.0.0.1";
|
|
32
|
+
|
|
17
33
|
// A per-host SSH tunnel's Cloudflare tunnel NAME (its hostname reuses sshHostname with the host-ssh id).
|
|
18
34
|
export const hostSshTunnelName = (id: string): string => `host-ssh-${id}`;
|
|
19
35
|
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { claudeContract } from "./contracts/claude.contract.js";
|
|
|
10
10
|
import { codexContract } from "./contracts/codex.contract.js";
|
|
11
11
|
import { draftsContract } from "./contracts/drafts.contract.js";
|
|
12
12
|
import { extensionsContract } from "./contracts/extensions.contract.js";
|
|
13
|
+
import { gateContract } from "./contracts/gate.contract.js";
|
|
13
14
|
import { geminiContract } from "./contracts/gemini.contract.js";
|
|
14
15
|
import { gitContract } from "./contracts/git.contract.js";
|
|
15
16
|
import { grokContract } from "./contracts/grok.contract.js";
|
|
@@ -41,6 +42,7 @@ export { claudeContract } from "./contracts/claude.contract.js";
|
|
|
41
42
|
export { codexContract } from "./contracts/codex.contract.js";
|
|
42
43
|
export { draftsContract } from "./contracts/drafts.contract.js";
|
|
43
44
|
export { extensionsContract } from "./contracts/extensions.contract.js";
|
|
45
|
+
export { gateContract } from "./contracts/gate.contract.js";
|
|
44
46
|
export { geminiContract } from "./contracts/gemini.contract.js";
|
|
45
47
|
export { gitContract } from "./contracts/git.contract.js";
|
|
46
48
|
export { grokContract } from "./contracts/grok.contract.js";
|
|
@@ -92,6 +94,7 @@ export const sandboxContract = {
|
|
|
92
94
|
settings: settingsContract,
|
|
93
95
|
intentic: intenticContract,
|
|
94
96
|
gemini: geminiContract,
|
|
97
|
+
gate: gateContract,
|
|
95
98
|
git: gitContract,
|
|
96
99
|
grok: grokContract,
|
|
97
100
|
kimi: kimiContract,
|
package/src/model-order.test.ts
CHANGED
|
@@ -7,37 +7,42 @@ import { compareCheapestFirst, compareModelIds, compareUnrankedModelIds, familyO
|
|
|
7
7
|
* conversations on whichever id sorted first. */
|
|
8
8
|
|
|
9
9
|
// A Codex catalog exactly as an OpenAI-compatible /v1/models hands it over: alphabetical, i.e. meaningless.
|
|
10
|
-
const CODEX = ["gpt-5.1-codex", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra"];
|
|
10
|
+
const CODEX = ["gpt-5.1-codex", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"];
|
|
11
11
|
|
|
12
12
|
test("ranks the frontier line above the cheap one and the newest release above its predecessors", () => {
|
|
13
13
|
// The base line (no tier word) leads, newest first; the mini rung sinks under all of it regardless of how
|
|
14
14
|
// recently it shipped — which is the whole decision a user makes in this list.
|
|
15
|
-
expect(CODEX.toSorted(compareModelIds)).toEqual(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.5", "gpt-5.1-codex", "gpt-5.4-mini"]);
|
|
15
|
+
expect(CODEX.toSorted(compareModelIds)).toEqual(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5", "gpt-5.1-codex", "gpt-5.4-mini"]);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
test("
|
|
19
|
-
// Arrival order survives only as the tiebreak between two ids the rule ranks equally (the 5.6 siblings), so
|
|
20
|
-
// an alphabetical registry and a reversed one can no longer disagree about which model the group opens on.
|
|
18
|
+
test("orders a release's named tiers strongest-first whichever order the endpoint listed them in", () => {
|
|
21
19
|
for (const arrival of [CODEX.toSorted(), CODEX.toReversed()]) {
|
|
22
20
|
const ordered = arrival.toSorted(compareModelIds);
|
|
23
21
|
|
|
24
|
-
expect(ordered.slice(0,
|
|
22
|
+
expect(ordered.slice(0, 3)).toEqual(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]);
|
|
25
23
|
expect(ordered.at(-1)).toBe("gpt-5.4-mini");
|
|
26
24
|
}
|
|
27
25
|
});
|
|
28
26
|
|
|
29
|
-
test("
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
test("keeps release-local tiers below the next generation and above the previous one", () => {
|
|
28
|
+
expect(["gpt-5.6-luna", "gpt-5.5", "gpt-5.7", "gpt-5.6-sol"].toSorted(compareModelIds)).toEqual([
|
|
29
|
+
"gpt-5.7",
|
|
30
|
+
"gpt-5.6-sol",
|
|
31
|
+
"gpt-5.6-luna",
|
|
32
|
+
"gpt-5.5",
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("the Codex release-tier order is stable across catalog refreshes", () => {
|
|
33
37
|
const arrivals = [
|
|
34
38
|
["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"],
|
|
35
39
|
["gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.6-sol"],
|
|
36
40
|
["gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"],
|
|
37
41
|
];
|
|
38
|
-
const heads = arrivals.map((arrival) => arrival.toSorted(compareUnrankedModelIds)[0]);
|
|
39
42
|
|
|
40
|
-
|
|
43
|
+
for (const arrival of arrivals) {
|
|
44
|
+
expect(arrival.toSorted(compareUnrankedModelIds)).toEqual(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]);
|
|
45
|
+
}
|
|
41
46
|
// Same rule, so ranking still outranks the tiebreak: the mini rung stays at the tail, under every sibling.
|
|
42
47
|
expect(["gpt-5.4-mini", ...arrivals[0]!].toSorted(compareUnrankedModelIds).at(-1)).toBe("gpt-5.4-mini");
|
|
43
48
|
});
|
|
@@ -68,6 +73,14 @@ test("reads each vendor's tier vocabulary, not just Claude's", () => {
|
|
|
68
73
|
]);
|
|
69
74
|
});
|
|
70
75
|
|
|
76
|
+
test("reads Kimi's k-prefixed generation so K3 leads the K2.x catalog", () => {
|
|
77
|
+
const catalog = ["kimi-k2.6", "kimi-k2.7-code-highspeed", "kimi-k3", "kimi-k2.7-code"];
|
|
78
|
+
|
|
79
|
+
expect(releaseOf("kimi-k3")).toEqual({ version: [3], date: 0 });
|
|
80
|
+
expect(familyOf("kimi-k3")).toBe(familyOf("kimi-k2.6"));
|
|
81
|
+
expect(catalog.toSorted(compareUnrankedModelIds)).toEqual(["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6"]);
|
|
82
|
+
});
|
|
83
|
+
|
|
71
84
|
test("the rightmost tier word wins, because tier words compose", () => {
|
|
72
85
|
// flash-lite is the cheap end of Flash, codex-max the frontier end of Codex — reading the leftmost word
|
|
73
86
|
// instead would file both under the tier they modify.
|
|
@@ -169,6 +182,10 @@ test("finds each vendor's own cheap rung, including a re-served open-weights row
|
|
|
169
182
|
expect(["grok-4", "grok-4-fast"].toSorted(compareCheapestFirst)[0]).toBe("grok-4-fast");
|
|
170
183
|
});
|
|
171
184
|
|
|
185
|
+
test("reads a release-local tier ladder from the cheap end too", () => {
|
|
186
|
+
expect(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"].toSorted(compareCheapestFirst)[0]).toBe("gpt-5.6-luna");
|
|
187
|
+
});
|
|
188
|
+
|
|
172
189
|
test("falls back on the newest release for a catalog that publishes no cheap tier at all", () => {
|
|
173
190
|
// Kimi names no tier word anywhere, so every row is UNRANKED and the tier term cancels. Serving the newest
|
|
174
191
|
// of what it does publish is the honest answer — there is no cheaper rung to find.
|
package/src/model-order.ts
CHANGED
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
* every vendor names its models the same way — which is what lets the daemon's four catalog services and the
|
|
15
15
|
* web's picker share one rule instead of each inventing a local one. */
|
|
16
16
|
|
|
17
|
-
// A version-ish segment: digits and dots, optionally
|
|
18
|
-
//
|
|
19
|
-
|
|
17
|
+
// A version-ish segment: digits and dots, optionally prefixed by the vendor's version marker (`4`, `5.1`, `v2`,
|
|
18
|
+
// `k3`, `k2.7`, `20251001`). Kimi is the one provider that fuses the marker with the generation; treating `k3`
|
|
19
|
+
// as a name made the current flagship look unversioned, so K2.x sorted above it. Everything else is a NAME
|
|
20
|
+
// segment and belongs to the family — which is what makes the split below exhaustive.
|
|
21
|
+
const VERSION_SEGMENT = /^(?:v|k)?[\d.]+$/i;
|
|
20
22
|
|
|
21
23
|
// A date stamp rather than a version component: six digits or more (20251001, 250514). The distinction is not
|
|
22
24
|
// cosmetic — claude-opus-4-1-20250805 (Opus 4.1) and claude-opus-4-20250514 (Opus 4.0) compare as (4,1) vs (4)
|
|
@@ -48,7 +50,7 @@ export interface ModelRelease {
|
|
|
48
50
|
export const releaseOf = (id: string): ModelRelease => {
|
|
49
51
|
const numeric = segmentsOf(id)
|
|
50
52
|
.filter((segment) => VERSION_SEGMENT.test(segment))
|
|
51
|
-
.map((segment) => segment.replace(/^
|
|
53
|
+
.map((segment) => segment.replace(/^[vk]/i, ""));
|
|
52
54
|
const stamps = numeric.filter((segment) => DATE_SEGMENT.test(segment)).map(Number);
|
|
53
55
|
return {
|
|
54
56
|
version: numeric
|
|
@@ -109,12 +111,21 @@ const TIER_RANK: Readonly<Record<string, number>> = {
|
|
|
109
111
|
|
|
110
112
|
const UNRANKED = -1;
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
/* Some providers name a capability ladder INSIDE one release instead of using the cross-release adjectives
|
|
115
|
+
* above. Codex 5.6's Sol/Terra/Luna rows are that shape: they must remain together ahead of the older 5.5 line,
|
|
116
|
+
* but their order is not an arbitrary id tiebreak — Sol is the strongest, followed by Terra, then Luna. Keeping
|
|
117
|
+
* this as a separate rank lets release recency still win across generations (a future GPT 5.7 base model must
|
|
118
|
+
* not be buried under a recognized 5.6 suffix), while the three siblings sort by their real tier. */
|
|
119
|
+
const RELEASE_TIER_RANK: Readonly<Record<string, number>> = {
|
|
120
|
+
sol: 0,
|
|
121
|
+
terra: 1,
|
|
122
|
+
luna: 2,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const lastRankOf = (family: string, ranks: Readonly<Record<string, number>>): number => {
|
|
115
126
|
let rank = UNRANKED;
|
|
116
127
|
for (const segment of family.split("-")) {
|
|
117
|
-
const found =
|
|
128
|
+
const found = ranks[segment];
|
|
118
129
|
if (found !== undefined) {
|
|
119
130
|
rank = found;
|
|
120
131
|
}
|
|
@@ -122,18 +133,32 @@ export const tierRankOf = (family: string): number => {
|
|
|
122
133
|
return rank;
|
|
123
134
|
};
|
|
124
135
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
const releaseTierRankOf = (family: string): number => lastRankOf(family, RELEASE_TIER_RANK);
|
|
137
|
+
|
|
138
|
+
// The LAST recognized word wins, because tier words compose and the rightmost is the most specific one:
|
|
139
|
+
// gemini-flash-lite is the cheap end of Flash, gpt-codex-max the frontier end of Codex.
|
|
140
|
+
export const tierRankOf = (family: string): number => lastRankOf(family, TIER_RANK);
|
|
141
|
+
|
|
142
|
+
// The canonical order of two model ids: broad tier first, then release, then a tier declared within that release.
|
|
143
|
+
// Hand it straight to Array#toSorted — that sort is stable, so two ids this rule cannot separate keep the order
|
|
144
|
+
// they arrived in (for Claude, the provider's own).
|
|
145
|
+
export const compareModelIds = (left: string, right: string): number => {
|
|
146
|
+
const leftFamily = familyOf(left);
|
|
147
|
+
const rightFamily = familyOf(right);
|
|
148
|
+
return (
|
|
149
|
+
tierRankOf(leftFamily) - tierRankOf(rightFamily) ||
|
|
150
|
+
compareRelease(releaseOf(left), releaseOf(right)) ||
|
|
151
|
+
releaseTierRankOf(leftFamily) - releaseTierRankOf(rightFamily)
|
|
152
|
+
);
|
|
153
|
+
};
|
|
129
154
|
|
|
130
155
|
/* The order for a catalog its endpoint published as a SET — Codex, Gemini, Kimi and Grok, i.e. everything but
|
|
131
156
|
* Anthropic's ranked list. Falling back on arrival order is what the rule above does with a tie, and for a RANKED
|
|
132
157
|
* catalog that is exactly right: the tie is the provider's own opinion, so claude-opus-5 stays ahead of
|
|
133
158
|
* claude-fable-5. For a set there is no opinion to keep, and the header of this file assumed the leftover order
|
|
134
|
-
* was at least alphabetical — it is not. A subscription
|
|
135
|
-
*
|
|
136
|
-
*
|
|
159
|
+
* was at least alphabetical — it is not. A subscription can hand tied rows back in whatever order its registry
|
|
160
|
+
* iterated THIS request, so the tie decides which model a fresh conversation opens on and can flip between
|
|
161
|
+
* catalog refreshes.
|
|
137
162
|
*
|
|
138
163
|
* So a set breaks its own ties on the id. Which sibling that seats first is arbitrary — but it is the same
|
|
139
164
|
* arbitrary answer every refresh, which is the property `default` actually needs. */
|
|
@@ -151,5 +176,12 @@ export const compareUnrankedModelIds = (left: string, right: string): number =>
|
|
|
151
176
|
* not the efficient rung — and the cheap end is only ever a family whose tier word is actually recognized.
|
|
152
177
|
* Falling off the end of a catalog with no efficient tier at all (Kimi publishes none) is then honest: the
|
|
153
178
|
* newest of what it does publish, chosen by the release tiebreak below. */
|
|
154
|
-
export const compareCheapestFirst = (left: string, right: string): number =>
|
|
155
|
-
|
|
179
|
+
export const compareCheapestFirst = (left: string, right: string): number => {
|
|
180
|
+
const leftFamily = familyOf(left);
|
|
181
|
+
const rightFamily = familyOf(right);
|
|
182
|
+
return (
|
|
183
|
+
tierRankOf(rightFamily) - tierRankOf(leftFamily) ||
|
|
184
|
+
compareRelease(releaseOf(left), releaseOf(right)) ||
|
|
185
|
+
releaseTierRankOf(rightFamily) - releaseTierRankOf(leftFamily)
|
|
186
|
+
);
|
|
187
|
+
};
|
package/src/quick-model.test.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type QuickModelSource, quickModelKey, resolveQuickModel } from "./quick
|
|
|
9
9
|
const CLAUDE: QuickModelSource = { provider: `claude`, ready: true, models: [`claude-opus-5`, `claude-sonnet-5`, `claude-haiku-4-5-20251001`] };
|
|
10
10
|
const GOOGLE: QuickModelSource = { provider: `gemini`, ready: true, models: [`gemini-3-flash`, `gemini-3-flash-lite`, `gemini-3-pro`] };
|
|
11
11
|
const CODEX: QuickModelSource = { provider: `codex`, ready: true, models: [`gpt-5.4-mini`, `gpt-5.6`] };
|
|
12
|
-
const KIMI: QuickModelSource = { provider: `kimi`, ready: true, models: [`kimi-k2
|
|
12
|
+
const KIMI: QuickModelSource = { provider: `kimi`, ready: true, models: [`kimi-k2.6`, `kimi-k2.7-code`, `kimi-k3`] };
|
|
13
13
|
|
|
14
14
|
const offline = (source: QuickModelSource): QuickModelSource => ({ ...source, ready: false });
|
|
15
15
|
|
|
@@ -72,7 +72,7 @@ test("ignores a malformed pin instead of running an empty model id", () => {
|
|
|
72
72
|
|
|
73
73
|
test("serves the newest of a catalog that publishes no cheap tier at all", () => {
|
|
74
74
|
// Kimi names no tier word anywhere. There is no cheaper rung to find, so the newest row is the honest answer.
|
|
75
|
-
expect(resolveQuickModel([KIMI], ``)).toEqual({ provider: `kimi`, model: `kimi-
|
|
75
|
+
expect(resolveQuickModel([KIMI], ``)).toEqual({ provider: `kimi`, model: `kimi-k3` });
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
test("reports nothing when no account is connected, so the button can say so instead of failing on click", () => {
|
package/src/schemas.test.ts
CHANGED
|
@@ -29,6 +29,11 @@ test("a payload from a build that predates a toggle parses, with the new toggle
|
|
|
29
29
|
autoLand: true,
|
|
30
30
|
autoResumeOnLimit: false,
|
|
31
31
|
resumeAfterOutage: true,
|
|
32
|
+
autoResumeOnRestart: true,
|
|
33
|
+
gateCommand: "",
|
|
34
|
+
gateQuietMs: 20_000,
|
|
35
|
+
gateTimeoutMs: 900_000,
|
|
36
|
+
gateAutoFix: true,
|
|
32
37
|
});
|
|
33
38
|
});
|
|
34
39
|
|
|
@@ -61,6 +66,15 @@ test("an empty object is the full default settings object", () => {
|
|
|
61
66
|
// On, unlike the limit resume beside it: an outage resume spends nothing the dead turn hadn't already
|
|
62
67
|
// committed, and the turns it saves are the unattended ones nobody is watching to restart by hand.
|
|
63
68
|
resumeAfterOutage: true,
|
|
69
|
+
// On: a daemon restart is usually intentic's own doing (an image update, an approved environment
|
|
70
|
+
// change), not the user's decision, so the turn it interrupted resumes rather than staying stuck.
|
|
71
|
+
autoResumeOnRestart: true,
|
|
72
|
+
// Empty disables the landing gate until the owner supplies this workspace's verification command.
|
|
73
|
+
gateCommand: "",
|
|
74
|
+
gateQuietMs: 20_000,
|
|
75
|
+
gateTimeoutMs: 900_000,
|
|
76
|
+
// Once a gate is configured, a red verdict wakes one fixer by default.
|
|
77
|
+
gateAutoFix: true,
|
|
64
78
|
});
|
|
65
79
|
});
|
|
66
80
|
|
package/src/schemas.ts
CHANGED
|
@@ -715,6 +715,45 @@ export const SandboxSettingsSchema = z.object({
|
|
|
715
715
|
* (automation wakes, Discord, webhooks), which no browser-held preference could ever rescue. It is the same
|
|
716
716
|
* reasoning that leaves the auth resume ungated: this is the provider's failure, not the user's decision. */
|
|
717
717
|
resumeAfterOutage: z.boolean().default(true),
|
|
718
|
+
/* When the daemon dies under a running turn, re-run that turn once it is back (agent/turn-journal.ts records
|
|
719
|
+
* every in-flight turn; the boot pass in agent/turn-resume.ts re-runs what survived). ON by default, where
|
|
720
|
+
* autoResumeOnLimit is off, and the difference is who broke the turn: a spent allowance is the user's own
|
|
721
|
+
* budget, while a restart is usually intentic's OWN doing — the container is recreated on every update,
|
|
722
|
+
* every environment approval and every dev-sandbox.sh swap. Approving the Dockerfile change an agent asked
|
|
723
|
+
* for must not cost the run that asked for it, and a user who just clicked Approve is in the room expecting
|
|
724
|
+
* the work to continue, not a second button.
|
|
725
|
+
*
|
|
726
|
+
* OFF still records the interruption: the fleet card reads `interrupted` (see AgentStatusSchema) and an
|
|
727
|
+
* automation's row shows an `interrupted` run — nothing is re-run, but nothing is silently lost either. */
|
|
728
|
+
autoResumeOnRestart: z.boolean().default(true),
|
|
729
|
+
/* THE LANDING GATE — the check command run over the COMPOSITE of landed work, once the fleet goes quiet.
|
|
730
|
+
* Empty ⇒ no gate at all, which is the default: only the owner knows what verifies this workspace, and a
|
|
731
|
+
* guessed command that fails on a fresh clone would read as the gate finding a bug on its first run.
|
|
732
|
+
*
|
|
733
|
+
* Configuring it is the opt-in, which is why there is no separate enable flag to disagree with it. The
|
|
734
|
+
* command runs in the workspace root through `sh -c`, exactly as a terminal would run it (see gate/gate.ts
|
|
735
|
+
* for why this is NOT an automation guard: a suite outlives GUARD_TIMEOUT_MS, and a timed-out guard reads
|
|
736
|
+
* as "skipped" — a silent green over a suite that never finished). */
|
|
737
|
+
gateCommand: z.string().max(500).default(""),
|
|
738
|
+
/* How long after a land the gate waits before running. A landing burst is the case this exists for: five
|
|
739
|
+
* agents finishing within a minute of each other are five lands, and a gate that ran per land would spend
|
|
740
|
+
* five suites to answer about four trees nobody will ever push. Every land re-arms the timer, so the run
|
|
741
|
+
* happens once, on the tree the user is about to review.
|
|
742
|
+
*
|
|
743
|
+
* It counts from the last LAND and nothing else — explicitly not "until the fleet is idle". Agents here run
|
|
744
|
+
* for hours, so a fleet of twenty with one long runner would never present a quiet moment, and a gate that
|
|
745
|
+
* waited for one would only ever fire when clicked (gate/gate.ts). */
|
|
746
|
+
gateQuietMs: z.number().min(0).max(600_000).default(20_000),
|
|
747
|
+
// Ceiling on one gate run, after which the child is killed and the verdict is `failed` with `timedOut`.
|
|
748
|
+
// Never a pass: a suite that did not finish has not said anything about the tree, and the one thing this
|
|
749
|
+
// gate exists to prevent is a green light nobody earned.
|
|
750
|
+
gateTimeoutMs: z.number().min(60_000).max(3_600_000).default(900_000),
|
|
751
|
+
/* Wake a fixer automatically when the gate goes red, instead of only lighting the badge. ON with a
|
|
752
|
+
* configured command, unlike the other unattended-spend toggles (autoResumeOnLimit), and the difference is
|
|
753
|
+
* that the spend here is the POINT: a red gate whose fix waits for the user to notice has moved the CI
|
|
754
|
+
* round-trip into the workspace without removing it from the user's day. One attempt per verdict, so a
|
|
755
|
+
* command that fails for a reason no agent can fix costs one turn, not a loop (gate/gate.ts). */
|
|
756
|
+
gateAutoFix: z.boolean().default(true),
|
|
718
757
|
});
|
|
719
758
|
export type SandboxSettings = z.infer<typeof SandboxSettingsSchema>;
|
|
720
759
|
|
|
@@ -1204,8 +1243,9 @@ export type WorkspaceClassification = z.infer<typeof WorkspaceClassificationSche
|
|
|
1204
1243
|
// within `text` so clients highlight without re-finding the needle.
|
|
1205
1244
|
export const WorkspaceSearchQuerySchema = z.object({
|
|
1206
1245
|
query: z.string().min(2).max(512),
|
|
1207
|
-
// Search verbs only — anchor/git verbs (outline, context, log, who, …) are CLI-only surface.
|
|
1208
|
-
|
|
1246
|
+
// Search verbs only — anchor/git verbs (outline, context, log, who, …) are CLI-only surface. Natural language
|
|
1247
|
+
// has no verb of its own: `q` classifies the query and answers it semantically when the words call for it.
|
|
1248
|
+
mode: z.enum(["q", "find", "files", "def", "refs", "sym", "ast"]).optional(),
|
|
1209
1249
|
includeIgnored: z.stringbool().optional(),
|
|
1210
1250
|
limit: z.coerce.number().int().positive().optional(),
|
|
1211
1251
|
after: z.string().optional(),
|
|
@@ -1233,6 +1273,9 @@ export const WorkspaceSearchFreshnessSchema = z.object({
|
|
|
1233
1273
|
state: z.enum(["fresh", "building", "stale"]),
|
|
1234
1274
|
ageMs: z.number().optional(),
|
|
1235
1275
|
progress: z.number().optional(),
|
|
1276
|
+
// How many files the index has not caught up with, when it is stale. A count is reportable; "stale" alone
|
|
1277
|
+
// reads as a warning about the answer, which it almost never is.
|
|
1278
|
+
behind: z.number().optional(),
|
|
1236
1279
|
});
|
|
1237
1280
|
export type WorkspaceSearchFreshness = z.infer<typeof WorkspaceSearchFreshnessSchema>;
|
|
1238
1281
|
export const WorkspaceSearchResultSchema = z.object({
|
|
@@ -1244,8 +1287,12 @@ export const WorkspaceSearchResultSchema = z.object({
|
|
|
1244
1287
|
truncated: z.boolean(),
|
|
1245
1288
|
cursor: z.string().optional(),
|
|
1246
1289
|
hint: z.string().optional(),
|
|
1247
|
-
// Code-graph neighbors of the top hits (definition anchors +
|
|
1290
|
+
// Code-graph neighbors of the top hits (definition anchors + the strongest caller of each).
|
|
1248
1291
|
related: z.array(z.string()).optional(),
|
|
1292
|
+
// Ranked `path:line` anchors that placed but were NOT shown, best first — the answer often sits at rank 5–13,
|
|
1293
|
+
// behind groups the budget spent itself on. The text surface has always printed this map; a JSON caller could
|
|
1294
|
+
// not see it, so it had to page through `cursor` to learn what the terminal was told up front.
|
|
1295
|
+
candidates: z.array(z.string()).optional(),
|
|
1249
1296
|
// Run provenance for benchmarking: retrieval stages DISABLED this invocation (absent = full pipeline).
|
|
1250
1297
|
features: z.array(z.string()).optional(),
|
|
1251
1298
|
});
|
|
@@ -1999,9 +2046,15 @@ export const AutomationApprovalIdParamSchema = z.object({ id: z.string() });
|
|
|
1999
2046
|
|
|
2000
2047
|
export const AutomationRunSchema = z.object({
|
|
2001
2048
|
at: z.number(),
|
|
2002
|
-
// skipped = the guard said no; error = the guard passed but the agent turn surfaced an error
|
|
2003
|
-
outcome
|
|
2049
|
+
// skipped = the guard said no; error = the guard passed but the agent turn surfaced an error; interrupted =
|
|
2050
|
+
// the daemon died mid-wake, so the run reached no outcome of its own (see agent/turn-journal.ts). Without
|
|
2051
|
+
// that last one an interrupted fire records NOTHING and simply vanishes from the row's history, which reads
|
|
2052
|
+
// as "it never fired" — the one reading a 3 a.m. automation must not be given.
|
|
2053
|
+
outcome: z.enum(["completed", "skipped", "error", "interrupted"]),
|
|
2004
2054
|
detail: z.string().optional(),
|
|
2055
|
+
// The runtime session the wake ran in, so the row can open the transcript. Absent for a run that never
|
|
2056
|
+
// reached a provider (skipped by its guard) or whose provider minted no session before it died.
|
|
2057
|
+
sessionId: z.string().optional(),
|
|
2005
2058
|
});
|
|
2006
2059
|
export type AutomationRun = z.infer<typeof AutomationRunSchema>;
|
|
2007
2060
|
|
|
@@ -2038,8 +2091,18 @@ export const PipelineRunSchema = z.object({
|
|
|
2038
2091
|
project: z.string(),
|
|
2039
2092
|
// The vendor's numeric run/pipeline id — what rerun/cancel address.
|
|
2040
2093
|
runId: z.number(),
|
|
2041
|
-
// github's display_title (the commit
|
|
2094
|
+
// The run's headline: github's display_title (the commit subject, or the PR title when a PR triggered it),
|
|
2095
|
+
// gitlab's pipeline name or the head commit's subject. Absent ⇒ the view falls back to ref@sha.
|
|
2042
2096
|
title: z.string().optional(),
|
|
2097
|
+
// Who the vendor credits for the run — the actor who set it off, matching what both vendors' own UIs
|
|
2098
|
+
// show. The avatar is a vendor-hosted URL; absent ⇒ the view draws the author's initials instead.
|
|
2099
|
+
authorName: z.string().optional(),
|
|
2100
|
+
authorAvatarUrl: z.string().optional(),
|
|
2101
|
+
// What set the run off, in the vendor's own vocabulary: gitlab's pipeline `source` (push, schedule,
|
|
2102
|
+
// merge_request_event, web, api, trigger…) or github's `event` (push, pull_request, schedule,
|
|
2103
|
+
// workflow_dispatch…). Left raw rather than flattened into a shared enum — the vendor's word is the
|
|
2104
|
+
// precise one, and the view only calls it out when it isn't the everyday push.
|
|
2105
|
+
trigger: z.string().optional(),
|
|
2043
2106
|
branch: z.string(),
|
|
2044
2107
|
sha: z.string(),
|
|
2045
2108
|
status: PipelineStatusSchema,
|
|
@@ -2052,6 +2115,28 @@ export const PipelineRunSchema = z.object({
|
|
|
2052
2115
|
});
|
|
2053
2116
|
export type PipelineRun = z.infer<typeof PipelineRunSchema>;
|
|
2054
2117
|
|
|
2118
|
+
// One job inside a pipeline run. The view fetches these lazily (one extra call per visible run) so the list
|
|
2119
|
+
// endpoint stays cheap. Both GitHub Actions jobs and GitLab CI jobs normalize onto these fields.
|
|
2120
|
+
// `stage` is GitLab's native sequential grouping and is absent on GitHub — the Actions jobs API exposes no
|
|
2121
|
+
// `stage` and no `needs`, so the view instead layers GitHub jobs into execution waves off the timestamps
|
|
2122
|
+
// below (overlapping runtimes ⇒ ran in parallel). Both are epoch ms; absent while a job is still queued.
|
|
2123
|
+
export const PipelineJobSchema = z.object({
|
|
2124
|
+
name: z.string(),
|
|
2125
|
+
status: PipelineStatusSchema,
|
|
2126
|
+
stage: z.string().optional(),
|
|
2127
|
+
startedAt: z.number().optional(),
|
|
2128
|
+
finishedAt: z.number().optional(),
|
|
2129
|
+
durationSeconds: z.number().optional(),
|
|
2130
|
+
// The job's page on its host — the shortest path from "this step failed" to the log that says why.
|
|
2131
|
+
webUrl: z.string().optional(),
|
|
2132
|
+
});
|
|
2133
|
+
export type PipelineJob = z.infer<typeof PipelineJobSchema>;
|
|
2134
|
+
|
|
2135
|
+
export const CiJobsResponseSchema = z.object({
|
|
2136
|
+
jobs: z.array(PipelineJobSchema),
|
|
2137
|
+
});
|
|
2138
|
+
export type CiJobsResponse = z.infer<typeof CiJobsResponseSchema>;
|
|
2139
|
+
|
|
2055
2140
|
// One mapped repo's CI wiring state. `hookWarning` is the manual-setup story when webhook registration was
|
|
2056
2141
|
// refused (token scope, role) or impossible (no public URL): what happened plus the target URL + secret to
|
|
2057
2142
|
// paste into the repo's webhook settings — the git-access sshRegistrationWarning pattern.
|
|
@@ -2069,9 +2154,17 @@ export const CiRunsResponseSchema = z.object({
|
|
|
2069
2154
|
repos: z.array(CiRepoSchema),
|
|
2070
2155
|
// Newest first, across all mapped repos.
|
|
2071
2156
|
runs: z.array(PipelineRunSchema),
|
|
2157
|
+
// When the owner last opened the pipelines view. Rides the runs response so the rail can decide what is
|
|
2158
|
+
// NEW without a second call — a breakage older than this has already been seen and must not badge again.
|
|
2159
|
+
// Absent ⇒ never opened, so everything counts as unseen.
|
|
2160
|
+
seenAt: z.number().optional(),
|
|
2072
2161
|
});
|
|
2073
2162
|
export type CiRunsResponse = z.infer<typeof CiRunsResponseSchema>;
|
|
2074
2163
|
|
|
2164
|
+
// Stamping the view as read hands back the timestamp it wrote, so the client updates without a refetch.
|
|
2165
|
+
export const CiSeenResponseSchema = z.object({ seenAt: z.number() });
|
|
2166
|
+
export type CiSeenResponse = z.infer<typeof CiSeenResponseSchema>;
|
|
2167
|
+
|
|
2075
2168
|
// rerun/cancel/fix address a run by repo + vendor id; the daemon re-resolves repo → project + token per call,
|
|
2076
2169
|
// so a stale card can't act on a project the workspace no longer maps to.
|
|
2077
2170
|
export const CiRunParamSchema = z.object({ repo: z.string(), runId: z.number() });
|
|
@@ -2081,6 +2174,97 @@ export type CiRunParam = z.infer<typeof CiRunParamSchema>;
|
|
|
2081
2174
|
export const CiFixResponseSchema = z.object({ conversationId: z.string() });
|
|
2082
2175
|
export type CiFixResponse = z.infer<typeof CiFixResponseSchema>;
|
|
2083
2176
|
|
|
2177
|
+
/* ---- the landing gate: the workspace's own verdict on the composite of landed work ----
|
|
2178
|
+
*
|
|
2179
|
+
* WHERE THIS SITS, and why it is not one of the four other places it could:
|
|
2180
|
+
*
|
|
2181
|
+
* A fleet of 5-20 agents lands work into the main tree as UNCOMMITTED changes (agents/land.ts), the user
|
|
2182
|
+
* reviews and commits it by parts, pushes, and CI answers minutes later. This gate front-runs that answer by
|
|
2183
|
+
* asking the same question of the same artifact, before the push.
|
|
2184
|
+
*
|
|
2185
|
+
* NOT inside an agent's turn. An isolated worktree's `node_modules` reads as the MAIN checkout's, so a
|
|
2186
|
+
* monorepo's workspace links resolve cross-package imports to /work's sources rather than the worktree's edited
|
|
2187
|
+
* ones (agents/worktrees.ts). A suite run in a worktree therefore tests the agent's edits against everyone
|
|
2188
|
+
* else's UNEDITED siblings: it invents failures that don't exist and passes changes that break on the
|
|
2189
|
+
* composite, and two agents editing one contract each go green alone and red together. The composite is the
|
|
2190
|
+
* only honest artifact, and it exists in exactly one place — the main working tree.
|
|
2191
|
+
*
|
|
2192
|
+
* NOT at commit. The user commits BY PARTS, and a suite reads the worktree, not the index — so a verdict taken
|
|
2193
|
+
* at a partial commit describes a tree that never gets pushed as such. Commit is where a verdict is DISPLAYED
|
|
2194
|
+
* (ReviewPanel's badge), computed earlier.
|
|
2195
|
+
*
|
|
2196
|
+
* NOT at push. By then HEAD has moved, per-path attribution has expired (agents/origins.ts), and the agents may
|
|
2197
|
+
* be archived with their worktrees reclaimed — so the fix starts cold, in the same position `/ci/fix` is in.
|
|
2198
|
+
* That saves the CI round-trip and none of the context switch.
|
|
2199
|
+
*
|
|
2200
|
+
* So: after the land, before the staging — the one window where the artifact is what CI will see, attribution
|
|
2201
|
+
* is still live, and nobody is waiting on it. */
|
|
2202
|
+
|
|
2203
|
+
/* What the gate has to say about the tree right now.
|
|
2204
|
+
*
|
|
2205
|
+
* idle — no command configured, or nothing has run yet.
|
|
2206
|
+
* armed — work landed; the quiet period is counting down (see gateQuietMs).
|
|
2207
|
+
* running — the check is live. `output` grows as it streams.
|
|
2208
|
+
* passed — exited 0 over `fingerprint`.
|
|
2209
|
+
* failed — exited non-zero, or was killed by gateTimeoutMs (`timedOut`). The state a fix answers.
|
|
2210
|
+
* error — the gate itself could not run: the command was not spawnable. NOT a fix-able failure, because
|
|
2211
|
+
* there is nothing wrong with the code — the gate is misconfigured, and saying "tests failed"
|
|
2212
|
+
* would send an agent hunting a bug that isn't there.
|
|
2213
|
+
* cancelled — the user stopped the run, or the tree moved under it.
|
|
2214
|
+
*/
|
|
2215
|
+
export const GateStatusSchema = z.enum(["idle", "armed", "running", "passed", "failed", "error", "cancelled"]);
|
|
2216
|
+
export type GateStatus = z.infer<typeof GateStatusSchema>;
|
|
2217
|
+
|
|
2218
|
+
// An agent whose landed work the failure implicates. `paths` are its attributed files that the check's own
|
|
2219
|
+
// output NAMED — empty when the output named none of them, which is the honest shape for a failure that could
|
|
2220
|
+
// not be pinpointed (an integration break between two deltas, a suite that prints no paths at all): the agent
|
|
2221
|
+
// is listed because its work is in the tree under test, not because anything accused it.
|
|
2222
|
+
export const GateAgentSchema = z.object({
|
|
2223
|
+
agentId: z.string(),
|
|
2224
|
+
title: z.string().optional(),
|
|
2225
|
+
provider: AgentProviderSchema.optional(),
|
|
2226
|
+
paths: z.array(z.string()),
|
|
2227
|
+
});
|
|
2228
|
+
export type GateAgent = z.infer<typeof GateAgentSchema>;
|
|
2229
|
+
|
|
2230
|
+
// The fix turn one red verdict got. A MAIN-TREE turn, not an isolated conversation, so there is no
|
|
2231
|
+
// conversationId and no fleet card to open — the composite it must reproduce lives in the main working tree and
|
|
2232
|
+
// a fresh worktree branches from HEAD without it. `sessionId` is what makes the run readable after the fact,
|
|
2233
|
+
// the same thing an automation's run record carries for the same reason.
|
|
2234
|
+
export const GateFixSchema = z.object({
|
|
2235
|
+
startedAt: z.number(),
|
|
2236
|
+
sessionId: z.string().optional(),
|
|
2237
|
+
// `running` while the turn streams; `done` when it ended cleanly, whatever the re-check then said;
|
|
2238
|
+
// `error` when the turn itself failed (a provider outage, no credential), which is worth distinguishing
|
|
2239
|
+
// because it is the one case where re-running the fix could still help.
|
|
2240
|
+
outcome: z.enum(["running", "done", "error"]),
|
|
2241
|
+
detail: z.string().optional(),
|
|
2242
|
+
});
|
|
2243
|
+
export type GateFix = z.infer<typeof GateFixSchema>;
|
|
2244
|
+
|
|
2245
|
+
export const GateVerdictSchema = z.object({
|
|
2246
|
+
status: GateStatusSchema,
|
|
2247
|
+
// The command this verdict ran, echoed rather than read back from settings: a verdict read after the
|
|
2248
|
+
// setting changed still has to say what produced it.
|
|
2249
|
+
command: z.string(),
|
|
2250
|
+
startedAt: z.number().optional(),
|
|
2251
|
+
finishedAt: z.number().optional(),
|
|
2252
|
+
exitCode: z.number().optional(),
|
|
2253
|
+
timedOut: z.boolean().optional(),
|
|
2254
|
+
// The check's own output, tail-capped (GATE_OUTPUT_BYTES). The tail, not the head: a suite's verdict and
|
|
2255
|
+
// its failure summary are at the end, and a head-capped buffer of a chatty build is all progress lines.
|
|
2256
|
+
output: z.string(),
|
|
2257
|
+
/* WHICH TREE this verdict is about — HEAD plus the shape of every repo's uncommitted content. Recomputed
|
|
2258
|
+
* on read: when it no longer matches, the verdict is `stale` and the badge says so instead of asserting a
|
|
2259
|
+
* green light over a tree that has since moved. This is what keeps a passed verdict from outliving its
|
|
2260
|
+
* subject when the user edits, discards, or commits half of it. */
|
|
2261
|
+
fingerprint: z.string(),
|
|
2262
|
+
stale: z.boolean(),
|
|
2263
|
+
implicated: z.array(GateAgentSchema),
|
|
2264
|
+
fix: GateFixSchema.optional(),
|
|
2265
|
+
});
|
|
2266
|
+
export type GateVerdict = z.infer<typeof GateVerdictSchema>;
|
|
2267
|
+
|
|
2084
2268
|
// ---- drafts: agent-proposed posts awaiting owner approval (.intentic/drafts/<id>.json) ----
|
|
2085
2269
|
// One JSON file per draft. The AGENT creates drafts with its normal file tools — it can't call daemon routes,
|
|
2086
2270
|
// the same split as the environment proposal — while the daemon edits/deletes them on the owner's behalf, so
|
|
@@ -2206,8 +2390,12 @@ export const PortForwardResultSchema = z.object({ previewUrl: z.string().optiona
|
|
|
2206
2390
|
export type PortForwardResult = z.infer<typeof PortForwardResultSchema>;
|
|
2207
2391
|
|
|
2208
2392
|
// ---- terminal ----
|
|
2209
|
-
// EVERY
|
|
2210
|
-
// is the /system/terminal WebSocket, not oRPC)
|
|
2393
|
+
// EVERY live surface in the sandbox the web app's ONE global panel can show. Mostly tmux sessions (the
|
|
2394
|
+
// interactive I/O is the /system/terminal WebSocket, not oRPC), plus the agent's browser, which is not a
|
|
2395
|
+
// terminal at all — no more than a `process` row is — but IS the same question: what is running right now,
|
|
2396
|
+
// and can I look at it? One list, because the panel that answers that question is one panel.
|
|
2397
|
+
//
|
|
2398
|
+
// `shell` = a web-* session the user opened (numbered pill),
|
|
2211
2399
|
// `panel` = a panel-* dev-server session (labeled by its panel key, started via Start; running:false =
|
|
2212
2400
|
// untracked, e.g. a finished one-shot job's lingering shell), `agent` = an agent-* session the Claude agent's
|
|
2213
2401
|
// Bash commands run in (live-watchable, AI-marked in the UI; running:false once every window is a finished
|
|
@@ -2218,7 +2406,11 @@ export type PortForwardResult = z.infer<typeof PortForwardResultSchema>;
|
|
|
2218
2406
|
// process (a lingering shell after a crash reads false). A process row that maps to an installed extension's
|
|
2219
2407
|
// declared process carries extensionId+processName, the address for its /extensions start/stop routes. The
|
|
2220
2408
|
// `{name}` kill-route param is a bare string validated in the handler (a bad name is a BAD_REQUEST) since the
|
|
2221
|
-
// same charset gates a `tmux kill-session -t` shell-out.
|
|
2409
|
+
// same charset gates a `tmux kill-session -t` shell-out. `browser` = a `browser-<sdk session>` Chromium the
|
|
2410
|
+
// agent is driving through its @playwright/mcp tools (browser/browser-sessions.ts) — watchable live over the
|
|
2411
|
+
// /system/browser-view WebSocket, `running` while that Chromium is connected, and hidden from the strip by
|
|
2412
|
+
// the same rule as `agent`: it is a record of work, not a place. Its `label` is the page's own title and
|
|
2413
|
+
// `url` the page it is on, which is the one thing a browser pill has to say that a terminal pill does not.
|
|
2222
2414
|
//
|
|
2223
2415
|
// `activityAt` (epoch ms of the session's last output) and `exitCode` (the LAST window's exit status, absent
|
|
2224
2416
|
// while that pane still lives) are what let a finished session be READ rather than merely listed: the panel's
|
|
@@ -2228,12 +2420,14 @@ export type PortForwardResult = z.infer<typeof PortForwardResultSchema>;
|
|
|
2228
2420
|
export const TerminalSessionSchema = z.object({
|
|
2229
2421
|
name: z.string(),
|
|
2230
2422
|
label: z.string().optional(),
|
|
2231
|
-
kind: z.enum(["shell", "panel", "agent", "job", "process"]),
|
|
2423
|
+
kind: z.enum(["shell", "panel", "agent", "job", "process", "browser"]),
|
|
2232
2424
|
running: z.boolean(),
|
|
2233
2425
|
activityAt: z.number(),
|
|
2234
2426
|
exitCode: z.number().optional(),
|
|
2235
2427
|
extensionId: z.string().optional(),
|
|
2236
2428
|
processName: z.string().optional(),
|
|
2429
|
+
// Browser sessions only: the page the agent is on right now.
|
|
2430
|
+
url: z.string().optional(),
|
|
2237
2431
|
});
|
|
2238
2432
|
export const TerminalsListSchema = z.object({ sessions: z.array(TerminalSessionSchema) });
|
|
2239
2433
|
export type TerminalsList = z.infer<typeof TerminalsListSchema>;
|