@intentic/sandbox-contract 1.339.0 → 1.341.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 +220 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +70 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/ci.contract.d.ts +5 -0
- package/dist/contracts/ci.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.d.ts +320 -0
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.js +28 -0
- package/dist/contracts/git.contract.js.map +1 -1
- package/dist/contracts/runner.contract.d.ts +82 -2
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +35 -0
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +2 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +9 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +143 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +3 -0
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/agent-words.d.ts +1 -0
- package/dist/events/agent-words.d.ts.map +1 -1
- package/dist/events/agent-words.js +2 -2
- package/dist/events/agent-words.js.map +1 -1
- package/dist/events/review.d.ts +70 -0
- package/dist/events/review.d.ts.map +1 -0
- package/dist/events/review.js +29 -0
- package/dist/events/review.js.map +1 -0
- package/dist/events/system-events.d.ts +6 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/system-events.js +2 -0
- package/dist/events/system-events.js.map +1 -1
- package/dist/events/transcript.d.ts +210 -0
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +2 -0
- package/dist/events/transcript.js.map +1 -1
- package/dist/events/undelivered.d.ts +6 -0
- package/dist/events/undelivered.d.ts.map +1 -0
- package/dist/events/undelivered.js +9 -0
- package/dist/events/undelivered.js.map +1 -0
- package/dist/index.d.ts +703 -42
- package/dist/index.d.ts.map +1 -1
- package/dist/models/plan-pools.d.ts.map +1 -1
- package/dist/models/plan-pools.js +2 -2
- package/dist/models/plan-pools.js.map +1 -1
- package/dist/netd/generated/browser-wire.d.ts +2 -0
- package/dist/netd/generated/browser-wire.d.ts.map +1 -1
- package/dist/protocol/peer-dial.d.ts +2 -1
- package/dist/protocol/peer-dial.d.ts.map +1 -1
- package/dist/protocol/peer-dial.js.map +1 -1
- package/dist/schemas/ci.d.ts +16 -0
- package/dist/schemas/ci.d.ts.map +1 -1
- package/dist/schemas/ci.js +9 -0
- package/dist/schemas/ci.js.map +1 -1
- package/dist/schemas/git/index-run.d.ts +201 -0
- package/dist/schemas/git/index-run.d.ts.map +1 -0
- package/dist/schemas/git/index-run.js +48 -0
- package/dist/schemas/git/index-run.js.map +1 -0
- package/dist/schemas/providers/plan-limits.d.ts +2 -0
- package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
- package/dist/schemas/providers/plan-limits.js +4 -0
- package/dist/schemas/providers/plan-limits.js.map +1 -1
- package/dist/schemas/settings.d.ts +2 -0
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +1 -0
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/voice.d.ts +3 -0
- package/dist/schemas/voice.d.ts.map +1 -1
- package/dist/schemas/voice.js +29 -1
- package/dist/schemas/voice.js.map +1 -1
- package/dist/state/definition.d.ts +4 -0
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/text/packed-formats.d.ts +2 -0
- package/dist/text/packed-formats.d.ts.map +1 -0
- package/dist/text/packed-formats.js +8 -0
- package/dist/text/packed-formats.js.map +1 -0
- package/dist/text/transcript-fold.d.ts +1 -1
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +14 -25
- package/dist/text/transcript-fold.js.map +1 -1
- package/dist/text/transcript-inline.d.ts +8 -0
- package/dist/text/transcript-inline.d.ts.map +1 -0
- package/dist/text/transcript-inline.js +28 -0
- package/dist/text/transcript-inline.js.map +1 -0
- package/package.json +5 -5
- package/src/contracts/git.contract.ts +33 -0
- package/src/events/agent-events.ts +7 -0
- package/src/events/agent-words.test.ts +6 -0
- package/src/events/agent-words.ts +4 -2
- package/src/events/review.ts +44 -0
- package/src/events/system-events.ts +6 -0
- package/src/events/transcript.ts +3 -0
- package/src/events/undelivered.ts +15 -0
- package/src/models/plan-pools.test.ts +12 -0
- package/src/models/plan-pools.ts +3 -2
- package/src/netd/generated/browser-wire.json +13 -0
- package/src/netd/generated/browser-wire.ts +1 -1
- package/src/protocol/peer-dial.ts +4 -2
- package/src/schemas/ci.ts +15 -0
- package/src/schemas/git/index-run.ts +80 -0
- package/src/schemas/providers/plan-limits.ts +8 -0
- package/src/schemas/settings.ts +4 -0
- package/src/schemas/voice.ts +40 -1
- package/src/text/packed-formats.ts +17 -0
- package/src/text/transcript-fold.ts +20 -35
- package/src/text/transcript-inline.test.ts +43 -0
- package/src/text/transcript-inline.ts +38 -0
|
@@ -152,6 +152,19 @@
|
|
|
152
152
|
"type"
|
|
153
153
|
],
|
|
154
154
|
"type": "object"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"description": "Every binary message after this one is the pane's bytes deflated, for a browser that asked with `deflate=1` in\nthe socket's query: its unpacked length (4 bytes, big-endian), then raw deflate (RFC 1951) through a sync flush,\nall of one socket's messages one deflate stream. Sent first, before any pane bytes; a browser that never sees it\nreads binary messages as the raw bytes they always were, which is what an older netd sends whatever was asked.",
|
|
158
|
+
"properties": {
|
|
159
|
+
"type": {
|
|
160
|
+
"const": "deflate",
|
|
161
|
+
"type": "string"
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"required": [
|
|
165
|
+
"type"
|
|
166
|
+
],
|
|
167
|
+
"type": "object"
|
|
155
168
|
}
|
|
156
169
|
],
|
|
157
170
|
"title": "TerminalServerMessage"
|
|
@@ -54,7 +54,7 @@ export type TerminalClientMessage = { "type": "input", data: string, } | { "type
|
|
|
54
54
|
/**
|
|
55
55
|
* What a terminal socket sends as JSON text; the pane's own bytes travel as binary messages.
|
|
56
56
|
*/
|
|
57
|
-
export type TerminalServerMessage = { "type": "grid", cols: number, rows: number, } | { "type": "exit", code: number, reason?: string, } | { "type": "pong" };
|
|
57
|
+
export type TerminalServerMessage = { "type": "grid", cols: number, rows: number, } | { "type": "exit", code: number, reason?: string, } | { "type": "pong" } | { "type": "deflate" };
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* Where netd's tunnel stands.
|
|
@@ -42,8 +42,10 @@ const OPEN = 1;
|
|
|
42
42
|
// Structural interface a browser's WebSocket, node's global one, and a test's fake all satisfy without a cast.
|
|
43
43
|
export interface SocketLike {
|
|
44
44
|
readonly readyState: number;
|
|
45
|
-
/* `message` is here for the watchdog alone, which needs no more than the fact that one arrived.
|
|
46
|
-
|
|
45
|
+
/* `message` is here for the watchdog alone, which needs no more than the fact that one arrived. Two signatures, so a
|
|
46
|
+
socket typing its listeners per event (ws's) satisfies it as well as one with a single listener type. */
|
|
47
|
+
addEventListener(type: "close", listener: (event: { readonly code?: number }) => void): void;
|
|
48
|
+
addEventListener(type: "open" | "error" | "message", listener: () => void): void;
|
|
47
49
|
send(data: string): void;
|
|
48
50
|
close(code?: number, reason?: string): void;
|
|
49
51
|
}
|
package/src/schemas/ci.ts
CHANGED
|
@@ -164,12 +164,27 @@ export type MainFailureDecision = z.infer<typeof MainFailureDecisionSchema>;
|
|
|
164
164
|
|
|
165
165
|
// A main-line branch while its CI fails, as the daemon's one fix agent on it sees it (ci/main-fixer.ts): from the first
|
|
166
166
|
// job that failed until a later run of every workflow that failed on it passes, whatever else failed along the way.
|
|
167
|
+
// One workflow failing on a main-line branch, where a push starts several (GitHub): a later run of it passing takes it
|
|
168
|
+
// off the failure, so a pass of any other says nothing of it. A scheduled one (a nightly) is the case that needs
|
|
169
|
+
// spelling out: a push does not run it, and the board's own runs may all pass while it still fails.
|
|
170
|
+
export const CiMainFailureWorkflowSchema = z.object({
|
|
171
|
+
name: z.string().describe("The workflow, by the name the forge gives it. `ci` on a forge that runs one pipeline per commit."),
|
|
172
|
+
runId: z.number().describe("Its newest run that failed."),
|
|
173
|
+
jobs: z.array(z.string()).describe("Its jobs failing now, by name."),
|
|
174
|
+
});
|
|
175
|
+
export type CiMainFailureWorkflow = z.infer<typeof CiMainFailureWorkflowSchema>;
|
|
167
176
|
export const CiMainFailureSchema = z.object({
|
|
168
177
|
repo: z.string().describe("Which workspace repository."),
|
|
169
178
|
branch: z.string().describe("Which main-line branch."),
|
|
170
179
|
since: z.number().describe("When its first job failed, in milliseconds."),
|
|
171
180
|
runId: z.number().describe("The newest run that failed on it."),
|
|
172
181
|
jobs: z.array(z.string()).describe("The jobs failing on it now, by name: the newest failed run's failures, and any that failed since."),
|
|
182
|
+
workflows: z
|
|
183
|
+
.array(CiMainFailureWorkflowSchema)
|
|
184
|
+
.optional()
|
|
185
|
+
.describe(
|
|
186
|
+
"Each workflow failing on it, in the order they began failing, with its newest failed run and its jobs: the branch passes again only once a later run of every one of them passes. Absent from a daemon from before workflows were told apart.",
|
|
187
|
+
),
|
|
173
188
|
fixer: z
|
|
174
189
|
.string()
|
|
175
190
|
.optional()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CommandRunSchema } from "../ci.js";
|
|
3
|
+
import { RepoParamSchema } from "../shared.js";
|
|
4
|
+
import { CommitResultSchema, GitTargetSchema } from "./git.js";
|
|
5
|
+
|
|
6
|
+
// Stage-all and commit-all as runs the daemon keeps, not requests held open: over a tree of tens of thousands of
|
|
7
|
+
// untracked files `git add -A` alone outlives the browser's header deadline (seconds), and a request that dies there
|
|
8
|
+
// leaves git running with nobody told how it ended. Modelled on the push run (PushRunSchema): started and answered at
|
|
9
|
+
// once, polled for progress and verdict, one per repository, a second start of the same thing joining the first.
|
|
10
|
+
|
|
11
|
+
// The guardrail's line, past which a run stops before staging and asks: a stage that size is almost always a dependency
|
|
12
|
+
// or build tree nobody meant to commit. Either one trips it.
|
|
13
|
+
export const INDEX_RUN_FILE_LIMIT = 2_000;
|
|
14
|
+
export const INDEX_RUN_BYTE_LIMIT = 100 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
export const IndexRunOpSchema = z.enum(["stage", "commit"]);
|
|
17
|
+
export type IndexRunOp = z.infer<typeof IndexRunOpSchema>;
|
|
18
|
+
|
|
19
|
+
// Where a running run is, in the order it passes them; a plain commit skips counting and staging.
|
|
20
|
+
// waiting: queued behind another write on the same repository (an agent's land, another commit).
|
|
21
|
+
// counting: reading what the stage would take, for the guardrail.
|
|
22
|
+
// staging / committing: git is running.
|
|
23
|
+
// reading: re-reading the repository the commit just wrote, for the answer.
|
|
24
|
+
export const IndexRunPhaseSchema = z.enum(["waiting", "counting", "staging", "committing", "reading"]);
|
|
25
|
+
|
|
26
|
+
export const IndexRunDirSchema = z.object({
|
|
27
|
+
dir: z.string().describe("A top-level directory of the repository, or an empty string for the files directly at its root."),
|
|
28
|
+
files: z.number().describe("How many of the files the stage would take sit under it."),
|
|
29
|
+
bytes: z.number().optional().describe("Their combined size on disk. Absent when the run did not measure sizes, which it skips once the file count alone is over the line."),
|
|
30
|
+
});
|
|
31
|
+
export type IndexRunDir = z.infer<typeof IndexRunDirSchema>;
|
|
32
|
+
|
|
33
|
+
// What a held run found, so the question names the directories rather than only a number.
|
|
34
|
+
export const IndexRunHoldSchema = z.object({
|
|
35
|
+
files: z.number().describe("How many files the stage would take."),
|
|
36
|
+
bytes: z.number().optional().describe("Their combined size on disk, when measured."),
|
|
37
|
+
dirs: z.array(IndexRunDirSchema).describe("The largest top-level directories among them, biggest first, at most a handful."),
|
|
38
|
+
});
|
|
39
|
+
export type IndexRunHold = z.infer<typeof IndexRunHoldSchema>;
|
|
40
|
+
|
|
41
|
+
// Flat rather than a union on `op`: the route's path parameter merges into the input, which a discriminated union cannot
|
|
42
|
+
// take, and the one cross-field rule (a commit needs a message) is a refine.
|
|
43
|
+
export const IndexRunStartSchema = RepoParamSchema.extend({
|
|
44
|
+
op: IndexRunOpSchema.describe("Stage, or commit (staging `target` first when given)."),
|
|
45
|
+
target: GitTargetSchema.optional().describe(
|
|
46
|
+
"What to stage. For a stage, leave it out for the whole repository. For a commit, leave it out to record the index exactly as it stands, or give an empty object to stage everything first.",
|
|
47
|
+
),
|
|
48
|
+
message: z.string().min(1).optional().describe("The commit message. Required for a commit, refused for a stage."),
|
|
49
|
+
confirmed: z
|
|
50
|
+
.boolean()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe(
|
|
53
|
+
`Go ahead past the guardrail. Without it, a stage that would take more than ${String(INDEX_RUN_FILE_LIMIT)} files or ${String(INDEX_RUN_BYTE_LIMIT / 1024 / 1024)} MB stops before touching the index and settles holding what it found, for a person to confirm.`,
|
|
54
|
+
),
|
|
55
|
+
})
|
|
56
|
+
.refine((start) => start.target === undefined || start.target.paths === undefined || start.target.scope === undefined, {
|
|
57
|
+
message: "name paths or a scope, not both",
|
|
58
|
+
})
|
|
59
|
+
.refine((start) => (start.op === "commit") === (start.message !== undefined), { message: "a commit takes a message and a stage does not" });
|
|
60
|
+
export type IndexRunStart = z.infer<typeof IndexRunStartSchema>;
|
|
61
|
+
|
|
62
|
+
export const IndexRunSchema = CommandRunSchema.extend({
|
|
63
|
+
id: z.string().describe("This run's own id, new on every start that did not join one already going. Empty when idle."),
|
|
64
|
+
repo: z.string().describe("The repository this run is about, the same id the routes take."),
|
|
65
|
+
op: IndexRunOpSchema.optional().describe("What was started. Absent when idle."),
|
|
66
|
+
phase: IndexRunPhaseSchema.optional().describe("Where a running run is. Absent once it settles."),
|
|
67
|
+
files: z
|
|
68
|
+
.object({
|
|
69
|
+
total: z.number().optional().describe("How many files the stage takes, once counted."),
|
|
70
|
+
done: z.number().optional().describe("How many of them are staged so far. git stages a whole repository in one go, so this jumps from 0 to the total."),
|
|
71
|
+
})
|
|
72
|
+
.optional()
|
|
73
|
+
.describe("Progress in files, where known."),
|
|
74
|
+
reason: z.string().optional().describe("Why it did not go, in git's own words where git said something."),
|
|
75
|
+
held: IndexRunHoldSchema.optional().describe(
|
|
76
|
+
"Present when the run stopped at the guardrail before touching anything (status `error`): what it would have staged. Start again with `confirmed` to go ahead.",
|
|
77
|
+
),
|
|
78
|
+
result: CommitResultSchema.optional().describe("A commit's answer, the same one the commit call gives, once it passes."),
|
|
79
|
+
});
|
|
80
|
+
export type IndexRun = z.infer<typeof IndexRunSchema>;
|
|
@@ -148,6 +148,14 @@ export const ProviderRefusalSchema = z.object({
|
|
|
148
148
|
.describe(
|
|
149
149
|
"When the provider said to try again, in epoch seconds, for a spent allowance where it named one. Until then the refusal stands whatever a reading says; after it, it is over.",
|
|
150
150
|
),
|
|
151
|
+
// Codex's "To get more access now, send a request to your admin": a team's credit, which no usage endpoint reads,
|
|
152
|
+
// so a weekly pool at 57% says nothing about it (2026-10-17).
|
|
153
|
+
unmetered: z
|
|
154
|
+
.literal(true)
|
|
155
|
+
.optional()
|
|
156
|
+
.describe(
|
|
157
|
+
"Set on a spent allowance no usage reading measures, such as a credit an organisation's admin grants. No reading answers it: it stands until the provider's own reset, or until a turn is answered.",
|
|
158
|
+
),
|
|
151
159
|
});
|
|
152
160
|
export type ProviderRefusal = z.infer<typeof ProviderRefusalSchema>;
|
|
153
161
|
export const ProviderRefusalsSchema = z.object({
|
package/src/schemas/settings.ts
CHANGED
|
@@ -452,6 +452,10 @@ export const SandboxSettingsSchema = z.object({
|
|
|
452
452
|
outagePolicy: RetryPolicySchema.default("wait").describe(
|
|
453
453
|
"What happens to a turn the model provider's own failure killed. `wait` holds it for a press. `retry` re-runs it on the shared per-provider breaker, backing off between attempts. The sandbox-wide default; any one conversation can say otherwise. Worth `retry` for a sandbox whose work mostly happens with nobody in the room.",
|
|
454
454
|
),
|
|
455
|
+
// A vendor loop's hard cap on one turn; half an hour ended two Gemini children mid-work as failures (2026-10-17).
|
|
456
|
+
turnCapMinutes: z.number().int().min(5).max(1440).default(30).describe(
|
|
457
|
+
"The longest one turn may run on a loop the sandbox drives itself (OpenCode, an ACP agent, Pi, Muse) before it is ended, in minutes, however busy it keeps. A subagent's turn that reaches it is carried on in a fresh turn of the same session rather than ended as a failure. The Claude Code and Codex loops have no such cap.",
|
|
458
|
+
),
|
|
455
459
|
// The posture that used to live only in a browser tab; moving it here is what lets it fire with nothing open.
|
|
456
460
|
stopPolicy: RetryPolicySchema.default("wait").describe(
|
|
457
461
|
"What happens to a turn that stopped short with nothing to repair — a hung runtime, a crashed harness. `wait` holds it for a press. `retry` re-runs the held turn on a short ladder, standing down after three tries that got nowhere rather than looping forever.",
|
package/src/schemas/voice.ts
CHANGED
|
@@ -159,7 +159,13 @@ export const VoiceServerMessageSchema = z.discriminatedUnion("type", [
|
|
|
159
159
|
type: z.literal("audio"),
|
|
160
160
|
id: z.number(),
|
|
161
161
|
sampleRate: z.number(),
|
|
162
|
-
samples: z.number().describe("How many mono
|
|
162
|
+
samples: z.number().describe("How many mono samples the binary frame right after this one holds, as s16le or once decoded."),
|
|
163
|
+
codec: z
|
|
164
|
+
.literal("opus")
|
|
165
|
+
.optional()
|
|
166
|
+
.describe(
|
|
167
|
+
"Absent: the frame is s16le PCM. `opus`, only for a socket opened with `codec=opus`: the frame is the chunk's Opus packets, each a big-endian 16-bit length then the packet (opusPackets), one Opus stream per utterance and sample rate.",
|
|
168
|
+
),
|
|
163
169
|
}),
|
|
164
170
|
z.object({ type: z.literal("done"), id: z.number() }),
|
|
165
171
|
z.object({
|
|
@@ -170,3 +176,36 @@ export const VoiceServerMessageSchema = z.discriminatedUnion("type", [
|
|
|
170
176
|
z.object({ type: z.literal("error"), id: z.number().optional(), message: z.string() }),
|
|
171
177
|
]);
|
|
172
178
|
export type VoiceServerMessage = z.infer<typeof VoiceServerMessageSchema>;
|
|
179
|
+
|
|
180
|
+
// Opus on the voice sockets, for a client that asks with `codec=opus` where the daemon's /events hello names it among
|
|
181
|
+
// `voiceCodecs`: about a tenth of PCM's bytes (24 kbit/s for 16 kHz speech against 256, 33 for 24 kHz against 384).
|
|
182
|
+
// `/speech/stream` takes one 20 ms packet per binary frame; `/speech/say` sends each chunk's packets in one frame, framed
|
|
183
|
+
// by these two. A client that asks nothing is sent PCM, and an older daemon that ignores the ask sends PCM too.
|
|
184
|
+
|
|
185
|
+
/** Packets as one frame: each a big-endian 16-bit length, then the packet. */
|
|
186
|
+
export const framedOpus = (packets: readonly Uint8Array[]): Uint8Array<ArrayBuffer> => {
|
|
187
|
+
const frame = new Uint8Array(packets.reduce((total, packet) => total + 2 + packet.byteLength, 0));
|
|
188
|
+
const view = new DataView(frame.buffer);
|
|
189
|
+
let at = 0;
|
|
190
|
+
for (const packet of packets) {
|
|
191
|
+
view.setUint16(at, packet.byteLength);
|
|
192
|
+
frame.set(packet, at + 2);
|
|
193
|
+
at += 2 + packet.byteLength;
|
|
194
|
+
}
|
|
195
|
+
return frame;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/** The packets of a frame framedOpus made; a frame cut short ends at its last whole packet. */
|
|
199
|
+
export const opusPackets = (frame: Uint8Array): Uint8Array[] => {
|
|
200
|
+
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
|
201
|
+
const packets: Uint8Array[] = [];
|
|
202
|
+
for (let at = 0; at + 2 <= frame.byteLength;) {
|
|
203
|
+
const length = view.getUint16(at);
|
|
204
|
+
if (at + 2 + length > frame.byteLength) {
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
packets.push(frame.subarray(at + 2, at + 2 + length));
|
|
208
|
+
at += 2 + length;
|
|
209
|
+
}
|
|
210
|
+
return packets;
|
|
211
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// File formats whose bytes are already compressed (images, audio, video, archives, office documents, fonts): encoding
|
|
2
|
+
// them again spends CPU and saves next to nothing. The daemon stores them as they are in a ZIP it streams, and the
|
|
3
|
+
// browser sends them plain where it gzips the rest of an upload, so both read the one list here.
|
|
4
|
+
|
|
5
|
+
const PACKED = new Set(
|
|
6
|
+
(
|
|
7
|
+
"7z aac apk avif br bz2 cab deb docx dmg epub flac gif gz heic heif ico jar jpeg jpg jxl lz lz4 lzma m4a m4v mkv mov mp3 mp4 mpeg mpg " +
|
|
8
|
+
"odg odp ods odt ogg ogv opus pdf png pptx rar rpm tbz tgz txz webm webp whl woff woff2 xlsx xz zip zst"
|
|
9
|
+
).split(" "),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
/** Whether a file of this name holds bytes that are already compressed, by its extension. */
|
|
13
|
+
export const isPackedName = (name: string): boolean => {
|
|
14
|
+
const base = name.slice(name.lastIndexOf("/") + 1);
|
|
15
|
+
const dot = base.lastIndexOf(".");
|
|
16
|
+
return dot > 0 && PACKED.has(base.slice(dot + 1).toLowerCase());
|
|
17
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cancelledRequests, settledRequests } from "../policy/request-status.js";
|
|
2
2
|
import type { AgentEvent } from "../events/agent-events.js";
|
|
3
|
-
import type
|
|
3
|
+
import { drawInline, type InlineEvent, supersedeEarlier } from "./transcript-inline.js";
|
|
4
4
|
import {
|
|
5
5
|
REQUEST_FIELDS,
|
|
6
6
|
holdsRequest,
|
|
@@ -16,6 +16,7 @@ import { keptWarmLine, keptWarmNotice } from "../schemas/keep-warm.js";
|
|
|
16
16
|
import { handedOffLine, handedOffNotice } from "../schemas/providers/handoff.js";
|
|
17
17
|
import { isLandConflict } from "../events/land-conflict.js";
|
|
18
18
|
import { turnedAwayCode } from "../policy/turned-away.js";
|
|
19
|
+
import { undeliveredRow } from "../events/undelivered.js";
|
|
19
20
|
import { mentionedPathTokens } from "./mentions.js";
|
|
20
21
|
import { unspokenPromptRow } from "../events/agent-words.js";
|
|
21
22
|
import { needRowText } from "../events/need-wake.js";
|
|
@@ -197,15 +198,6 @@ const landedRow = (event: Extract<AgentEvent, { kind: "landed" }>): TranscriptRo
|
|
|
197
198
|
const failureOf = (event: Extract<AgentEvent, { kind: "error" }>): { message: string; error?: string } =>
|
|
198
199
|
event.code === undefined ? { message: event.message } : { message: event.message, error: event.code };
|
|
199
200
|
|
|
200
|
-
// The clause after a refusal that ran nothing. A turn somebody typed is held in its conversation's queue for another
|
|
201
|
-
// press; a turn that started itself — an automation, a loop, a watch wake — has no typed message and nobody watching,
|
|
202
|
-
// so promising a resend there names a composer the reader is not looking at. Neither version promises a retry: whether
|
|
203
|
-
// one comes is the scheduler's decision, not this row's to state.
|
|
204
|
-
const undelivered = (unattended: boolean): string =>
|
|
205
|
-
unattended
|
|
206
|
-
? `Nothing ran, and nothing is held: this run started on its own, so there is no message waiting to be sent again.`
|
|
207
|
-
: `Your message was not delivered: it is held for you to send again.`;
|
|
208
|
-
|
|
209
201
|
// The press a memory hold offers: the raise rides along only when the hold carried a ceiling to size it by, since a
|
|
210
202
|
// stall names none and raising one would not clear it.
|
|
211
203
|
const memoryPress = (event: Extract<AgentEvent, { kind: "error" }>): "sendAnyway" | "sandboxMemory" =>
|
|
@@ -230,11 +222,7 @@ const heldRow = (event: Extract<AgentEvent, { kind: "error" }>): TranscriptRow =
|
|
|
230
222
|
};
|
|
231
223
|
}
|
|
232
224
|
if (!memory || unattended) {
|
|
233
|
-
return
|
|
234
|
-
role: "notice",
|
|
235
|
-
text: `${event.message} ${undelivered(unattended)}`,
|
|
236
|
-
noticeCode: noticeCode({ code: "undelivered", params: { ...said, unattended: unattended || undefined } }),
|
|
237
|
-
};
|
|
225
|
+
return undeliveredRow(said, unattended);
|
|
238
226
|
}
|
|
239
227
|
return {
|
|
240
228
|
role: "notice",
|
|
@@ -462,12 +450,9 @@ export class TranscriptFold {
|
|
|
462
450
|
return this.steered(event);
|
|
463
451
|
case "checkpoint":
|
|
464
452
|
// Anchors the pre-turn snapshot id and this turn's transcript position on its user row, for rewind.
|
|
465
|
-
return this.stampOpener((row) =>
|
|
466
|
-
row
|
|
467
|
-
|
|
468
|
-
row.rewindIndex = event.index;
|
|
469
|
-
}
|
|
470
|
-
});
|
|
453
|
+
return this.stampOpener((row) =>
|
|
454
|
+
Object.assign(row, { checkpointId: event.id }, event.index === undefined ? {} : { rewindIndex: event.index }),
|
|
455
|
+
);
|
|
471
456
|
case "preamble":
|
|
472
457
|
// Collapses the daemon's preamble notes onto the user row; an empty note list is not a disclosure.
|
|
473
458
|
return event.notes.length === 0 ? [] : this.stampOpener((row) => (row.notes = [...event.notes]));
|
|
@@ -484,7 +469,7 @@ export class TranscriptFold {
|
|
|
484
469
|
case "handoff":
|
|
485
470
|
return this.pushRow({ role: "notice", text: handedOffLine(event), noticeCode: noticeCode(handedOffNotice(event)) });
|
|
486
471
|
case "worktree":
|
|
487
|
-
return
|
|
472
|
+
return this.synced(event.sync);
|
|
488
473
|
case "landed":
|
|
489
474
|
return this.pushRow(landedRow(event));
|
|
490
475
|
case "compact":
|
|
@@ -553,7 +538,8 @@ export class TranscriptFold {
|
|
|
553
538
|
case "credential_offer":
|
|
554
539
|
return this.park(event.requestId, { credentialOffer: { requestId: event.requestId, offer: event.offer, status: "pending" } });
|
|
555
540
|
case "page":
|
|
556
|
-
|
|
541
|
+
case "review":
|
|
542
|
+
return this.showInline(event);
|
|
557
543
|
case "page_ask":
|
|
558
544
|
return this.park(event.requestId, { pageAsk: { requestId: event.requestId, page: event.page, status: "pending" } });
|
|
559
545
|
case "need":
|
|
@@ -608,20 +594,16 @@ export class TranscriptFold {
|
|
|
608
594
|
}
|
|
609
595
|
|
|
610
596
|
/**
|
|
611
|
-
*
|
|
612
|
-
* next row. A redraw of
|
|
597
|
+
* Drawn on the bubble whose call showed it (a page, a review's findings), which it then closes, as a card does: the
|
|
598
|
+
* prose after it is the next row. A redraw of one already shown folds the earlier row to a line rather than drawing
|
|
599
|
+
* two copies (transcript-inline.ts).
|
|
613
600
|
*/
|
|
614
|
-
private
|
|
615
|
-
const
|
|
616
|
-
const earlier = this.rows.findLastIndex((row) => row.page !== undefined && row.page.id === page.id && row.page.superseded !== true);
|
|
617
|
-
if (earlier !== -1) {
|
|
618
|
-
this.rows[earlier]!.page = { ...this.rows[earlier]!.page!, superseded: true };
|
|
619
|
-
patches.push(this.replace(earlier));
|
|
620
|
-
}
|
|
601
|
+
private showInline(event: InlineEvent): TranscriptPatch[] {
|
|
602
|
+
const earlier = supersedeEarlier(this.rows, event);
|
|
621
603
|
const [index, opened] = this.open();
|
|
622
|
-
this.rows[index]
|
|
604
|
+
drawInline(this.rows[index]!, event);
|
|
623
605
|
this.bubble = undefined;
|
|
624
|
-
return [...
|
|
606
|
+
return [...(earlier === undefined ? [] : [this.replace(earlier)]), ...opened, this.replace(index)];
|
|
625
607
|
}
|
|
626
608
|
|
|
627
609
|
/** A message pushed into a running turn; only a person's own message is a rewind anchor. */
|
|
@@ -910,7 +892,10 @@ export class TranscriptFold {
|
|
|
910
892
|
|
|
911
893
|
// The rebase notice, less what a resolve turn was sent to fix: that turn opens because the branch couldn't rebase, so
|
|
912
894
|
// saying so again one row under its own errand only repeats it. What did move is still told.
|
|
913
|
-
private synced(sync: { commits: number; blocked: readonly string[] }): TranscriptPatch[] {
|
|
895
|
+
private synced(sync: { commits: number; blocked: readonly string[] } | undefined): TranscriptPatch[] {
|
|
896
|
+
if (sync === undefined) {
|
|
897
|
+
return [];
|
|
898
|
+
}
|
|
914
899
|
const opener = this.opener === undefined ? undefined : this.rows[this.opener];
|
|
915
900
|
const told = opener !== undefined && isLandConflict(opener.text) ? { ...sync, blocked: [] } : sync;
|
|
916
901
|
const text = syncLine(told);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AgentEvent } from "../events/agent-events.js";
|
|
2
|
+
import type { Page } from "../events/requests.js";
|
|
3
|
+
import type { ReviewFindings } from "../events/review.js";
|
|
4
|
+
import { foldTurn, userRow } from "./transcript-fold.js";
|
|
5
|
+
|
|
6
|
+
// path-literals: content, a page's stored path and a finding's file as transcript rows carry them
|
|
7
|
+
const PAGE: Page = { id: "p1", title: "Q3 revenue", path: ".intentic/records/artifacts/pages/c1/p1.r0.html" };
|
|
8
|
+
const REVIEW: ReviewFindings = {
|
|
9
|
+
id: "review-1",
|
|
10
|
+
source: "claude",
|
|
11
|
+
level: "high",
|
|
12
|
+
findings: [{ file: "src/parse.ts", line: 12, summary: "Off-by-one drops the last token.", verdict: "CONFIRMED" }],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const fold = (events: readonly AgentEvent[]) => foldTurn([userRow("/code-review high", 1, [])], events).slice(1);
|
|
16
|
+
|
|
17
|
+
describe("a review in the fold", () => {
|
|
18
|
+
it("draws the findings on the bubble that reported them, and starts the next words below", () => {
|
|
19
|
+
const rows = fold([
|
|
20
|
+
{ kind: "tool_call", id: "t1", name: "ReportFindings", category: "other", status: "in_progress" },
|
|
21
|
+
{ kind: "review", review: REVIEW },
|
|
22
|
+
{ kind: "delta", text: "One finding." },
|
|
23
|
+
]);
|
|
24
|
+
expect(rows).toHaveLength(2);
|
|
25
|
+
expect(rows[0]?.review).toEqual(REVIEW);
|
|
26
|
+
expect(rows[1]).toEqual({ role: "assistant", text: "One finding." });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// The skill re-reports the same findings once it has fixed them: the card is redrawn, not drawn twice.
|
|
30
|
+
it("folds the earlier card away when the same review is re-reported with its outcomes", () => {
|
|
31
|
+
const fixed: ReviewFindings = { ...REVIEW, findings: REVIEW.findings.map((finding) => ({ ...finding, outcome: "fixed" as const })) };
|
|
32
|
+
const rows = fold([{ kind: "review", review: REVIEW }, { kind: "delta", text: "Fixing." }, { kind: "text_end" }, { kind: "review", review: fixed }]);
|
|
33
|
+
expect(rows[0]?.review?.superseded).toBe(true);
|
|
34
|
+
expect(rows.at(-1)?.review).toEqual(fixed);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// A page and a review share the redraw rule by id, never each other's rows.
|
|
38
|
+
it("keeps a page and a review that share an id apart", () => {
|
|
39
|
+
const rows = fold([{ kind: "page", page: PAGE }, { kind: "review", review: { ...REVIEW, id: PAGE.id } }]);
|
|
40
|
+
expect(rows[0]?.page?.superseded).toBeUndefined();
|
|
41
|
+
expect(rows[1]?.review?.id).toBe(PAGE.id);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AgentEvent } from "../events/agent-events.js";
|
|
2
|
+
import type { TranscriptRow } from "../events/transcript.js";
|
|
3
|
+
|
|
4
|
+
// What the chat draws inline, on the bubble that showed it, rather than as a card that waits for an answer: a page the
|
|
5
|
+
// agent showed (show_page) and a code review's findings. Each has an id, and one shown again under its id is a redraw
|
|
6
|
+
// (a page redrawn, a review re-reported with its fixes applied), so the earlier row folds to a line.
|
|
7
|
+
|
|
8
|
+
export type InlineEvent = Extract<AgentEvent, { kind: "page" | "review" }>;
|
|
9
|
+
|
|
10
|
+
const drawnOf = (row: TranscriptRow, kind: InlineEvent["kind"]): { readonly id: string; readonly superseded?: boolean | undefined } | undefined =>
|
|
11
|
+
kind === "page" ? row.page : row.review;
|
|
12
|
+
|
|
13
|
+
// The latest row still drawing this one in full, folded now that its redraw stands below; undefined when none does.
|
|
14
|
+
export const supersedeEarlier = (rows: TranscriptRow[], event: InlineEvent): number | undefined => {
|
|
15
|
+
const id = event.kind === "page" ? event.page.id : event.review.id;
|
|
16
|
+
const index = rows.findLastIndex((row) => {
|
|
17
|
+
const drawn = drawnOf(row, event.kind);
|
|
18
|
+
return drawn !== undefined && drawn.id === id && drawn.superseded !== true;
|
|
19
|
+
});
|
|
20
|
+
const row = rows[index];
|
|
21
|
+
if (row === undefined) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
if (event.kind === "page" && row.page !== undefined) {
|
|
25
|
+
row.page = { ...row.page, superseded: true };
|
|
26
|
+
} else if (event.kind === "review" && row.review !== undefined) {
|
|
27
|
+
row.review = { ...row.review, superseded: true };
|
|
28
|
+
}
|
|
29
|
+
return index;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const drawInline = (row: TranscriptRow, event: InlineEvent): void => {
|
|
33
|
+
if (event.kind === "page") {
|
|
34
|
+
row.page = event.page;
|
|
35
|
+
} else {
|
|
36
|
+
row.review = event.review;
|
|
37
|
+
}
|
|
38
|
+
};
|