@intentic/sandbox-contract 1.303.0 → 1.305.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 +5 -3
- package/dist/contracts/agent.contract.d.ts +195 -41
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +8 -8
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +217 -3
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +30 -2
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +18 -5
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/capabilities.contract.js +3 -3
- package/dist/contracts/capabilities.contract.js.map +1 -1
- package/dist/contracts/{host.contract.d.ts → device.contract.d.ts} +75 -2
- package/dist/contracts/device.contract.d.ts.map +1 -0
- package/dist/contracts/device.contract.js +17 -0
- package/dist/contracts/device.contract.js.map +1 -0
- package/dist/contracts/extensions.contract.d.ts +2 -2
- package/dist/contracts/git.contract.d.ts +24 -0
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/panels.contract.d.ts +1 -1
- package/dist/contracts/personas.contract.d.ts +0 -9
- package/dist/contracts/personas.contract.d.ts.map +1 -1
- package/dist/contracts/personas.contract.js +1 -10
- package/dist/contracts/personas.contract.js.map +1 -1
- package/dist/contracts/runner.contract.d.ts +138 -98
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.js +1 -0
- package/dist/contracts/runner.contract.js.map +1 -1
- package/dist/contracts/secrets.contract.js +2 -2
- package/dist/contracts/secrets.contract.js.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +23 -3
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +0 -23
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +109 -8
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.js +11 -2
- package/dist/contracts/system.contract.js.map +1 -1
- package/dist/events/agent-events.d.ts +85 -10
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +26 -10
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/agent-words.d.ts +20 -0
- package/dist/events/agent-words.d.ts.map +1 -0
- package/dist/events/agent-words.js +82 -0
- package/dist/events/agent-words.js.map +1 -0
- package/dist/events/{cards.d.ts → requests.d.ts} +18 -18
- package/dist/events/requests.d.ts.map +1 -0
- package/dist/events/{cards.js → requests.js} +26 -26
- package/dist/events/requests.js.map +1 -0
- package/dist/events/resume.d.ts +1 -0
- package/dist/events/resume.d.ts.map +1 -1
- package/dist/events/resume.js +2 -0
- package/dist/events/resume.js.map +1 -1
- package/dist/events/system-events.d.ts +21 -3
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/transcript.d.ts +165 -25
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +34 -15
- package/dist/events/transcript.js.map +1 -1
- package/dist/events/watch-wake.d.ts.map +1 -1
- package/dist/events/watch-wake.js +4 -0
- package/dist/events/watch-wake.js.map +1 -1
- package/dist/index.d.ts +629 -129
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/models/model-offer.d.ts +2 -1
- package/dist/models/model-offer.d.ts.map +1 -1
- package/dist/models/model-offer.js +5 -4
- package/dist/models/model-offer.js.map +1 -1
- package/dist/models/model-roles.d.ts +2 -9
- package/dist/models/model-roles.d.ts.map +1 -1
- package/dist/models/model-roles.js +2 -9
- package/dist/models/model-roles.js.map +1 -1
- package/dist/policy/command-classes.d.ts +1 -0
- package/dist/policy/command-classes.d.ts.map +1 -1
- package/dist/policy/command-classes.js +8 -0
- package/dist/policy/command-classes.js.map +1 -1
- package/dist/policy/persona-home.d.ts +10 -0
- package/dist/policy/persona-home.d.ts.map +1 -0
- package/dist/policy/persona-home.js +11 -0
- package/dist/policy/persona-home.js.map +1 -0
- package/dist/policy/request-status.d.ts +5 -0
- package/dist/policy/request-status.d.ts.map +1 -0
- package/dist/policy/{card-status.js → request-status.js} +3 -3
- package/dist/policy/request-status.js.map +1 -0
- package/dist/policy/safety-policy.d.ts +1 -1
- package/dist/policy/safety-policy.d.ts.map +1 -1
- package/dist/policy/safety-policy.js +1 -0
- package/dist/policy/safety-policy.js.map +1 -1
- package/dist/policy/turned-away.d.ts +2 -0
- package/dist/policy/turned-away.d.ts.map +1 -0
- package/dist/policy/turned-away.js +13 -0
- package/dist/policy/turned-away.js.map +1 -0
- package/dist/protocol/peer-dial.d.ts +9 -0
- package/dist/protocol/peer-dial.d.ts.map +1 -1
- package/dist/protocol/peer-dial.js +12 -4
- package/dist/protocol/peer-dial.js.map +1 -1
- package/dist/protocol/peer-mcp-server.d.ts +1 -0
- package/dist/protocol/peer-mcp-server.d.ts.map +1 -1
- package/dist/protocol/peer-mcp-server.js +14 -1
- package/dist/protocol/peer-mcp-server.js.map +1 -1
- package/dist/schemas/agent.d.ts +2 -1
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +7 -0
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents.d.ts +29 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +18 -0
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/automations.d.ts +9 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/capabilities.d.ts +25 -12
- package/dist/schemas/capabilities.d.ts.map +1 -1
- package/dist/schemas/capabilities.js +11 -6
- package/dist/schemas/capabilities.js.map +1 -1
- package/dist/schemas/chat-route.d.ts +49 -0
- package/dist/schemas/chat-route.d.ts.map +1 -0
- package/dist/schemas/{model-route.js → chat-route.js} +17 -5
- package/dist/schemas/chat-route.js.map +1 -0
- package/dist/schemas/context-trim.d.ts +9 -0
- package/dist/schemas/context-trim.d.ts.map +1 -0
- package/dist/schemas/context-trim.js +18 -0
- package/dist/schemas/context-trim.js.map +1 -0
- package/dist/schemas/devices.d.ts +21 -8
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +8 -9
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/extension-updates.d.ts +4 -4
- package/dist/schemas/extension-updates.js +1 -1
- package/dist/schemas/extension-updates.js.map +1 -1
- package/dist/schemas/git/git.d.ts +83 -0
- package/dist/schemas/git/git.d.ts.map +1 -1
- package/dist/schemas/git/git.js +40 -0
- package/dist/schemas/git/git.js.map +1 -1
- package/dist/schemas/hosts.d.ts +36 -5
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +7 -5
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/metrics.d.ts +108 -0
- package/dist/schemas/metrics.d.ts.map +1 -0
- package/dist/schemas/metrics.js +77 -0
- package/dist/schemas/metrics.js.map +1 -0
- package/dist/schemas/panels.d.ts +2 -2
- package/dist/schemas/panels.js +1 -1
- package/dist/schemas/panels.js.map +1 -1
- package/dist/schemas/personas.d.ts +1 -13
- package/dist/schemas/personas.d.ts.map +1 -1
- package/dist/schemas/personas.js +3 -20
- package/dist/schemas/personas.js.map +1 -1
- package/dist/schemas/providers/plan-limits.d.ts +1 -0
- package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
- package/dist/schemas/providers/plan-limits.js +6 -0
- package/dist/schemas/providers/plan-limits.js.map +1 -1
- package/dist/schemas/secrets.d.ts +1 -1
- package/dist/schemas/secrets.d.ts.map +1 -1
- package/dist/schemas/secrets.js +1 -1
- package/dist/schemas/secrets.js.map +1 -1
- package/dist/schemas/settings.d.ts +0 -42
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +1 -16
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/shared.d.ts +2 -2
- package/dist/schemas/shared.d.ts.map +1 -1
- package/dist/schemas/shared.js +3 -3
- package/dist/schemas/shared.js.map +1 -1
- package/dist/schemas/system-prompt.d.ts +104 -0
- package/dist/schemas/system-prompt.d.ts.map +1 -0
- package/dist/schemas/system-prompt.js +25 -0
- package/dist/schemas/system-prompt.js.map +1 -0
- package/dist/schemas/workflows.d.ts +2 -2
- package/dist/schemas/workflows.d.ts.map +1 -1
- package/dist/schemas/workflows.js +2 -2
- package/dist/schemas/workflows.js.map +1 -1
- package/dist/state/definition.d.ts +18 -10
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/fix-attempt-plan.d.ts +4 -0
- package/dist/state/fix-attempt-plan.d.ts.map +1 -1
- package/dist/state/fix-attempt-plan.js +3 -2
- package/dist/state/fix-attempt-plan.js.map +1 -1
- package/dist/state/fix-stance.d.ts +1 -0
- package/dist/state/fix-stance.d.ts.map +1 -1
- package/dist/state/fix-stance.js +11 -0
- package/dist/state/fix-stance.js.map +1 -1
- package/dist/state/history-state.d.ts.map +1 -1
- package/dist/state/history-state.js +8 -1
- package/dist/state/history-state.js.map +1 -1
- package/dist/state/runtime-state.d.ts +0 -1
- package/dist/state/runtime-state.d.ts.map +1 -1
- package/dist/state/runtime-state.js +0 -1
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/state/workspace-state.d.ts +4 -4
- package/dist/state/workspace-state.d.ts.map +1 -1
- package/dist/state/workspace-state.js +4 -4
- package/dist/state/workspace-state.js.map +1 -1
- package/dist/text/documents.d.ts +2 -2
- package/dist/text/documents.d.ts.map +1 -1
- package/dist/text/documents.js.map +1 -1
- package/dist/text/mentions.d.ts +1 -0
- package/dist/text/mentions.d.ts.map +1 -1
- package/dist/text/mentions.js +5 -1
- package/dist/text/mentions.js.map +1 -1
- package/dist/text/transcript-fold.d.ts +8 -3
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +83 -28
- package/dist/text/transcript-fold.js.map +1 -1
- package/package.json +9 -9
- package/src/chores/chores.test.ts +1 -1
- package/src/chores/digest.test.ts +1 -1
- package/src/chores/probes.test.ts +5 -2
- package/src/chores/stack.test.ts +1 -1
- package/src/chores/verdict.test.ts +6 -5
- package/src/contracts/agent.contract.ts +10 -9
- package/src/contracts/agents.contract.ts +35 -2
- package/src/contracts/capabilities.contract.ts +3 -3
- package/src/contracts/{host.contract.ts → device.contract.ts} +8 -6
- package/src/contracts/personas.contract.ts +1 -13
- package/src/contracts/runner.contract.ts +3 -2
- package/src/contracts/secrets.contract.ts +2 -2
- package/src/contracts/system.contract.ts +13 -2
- package/src/contracts/webext.contract.ts +1 -1
- package/src/events/agent-events.ts +37 -14
- package/src/events/agent-words.test.ts +68 -0
- package/src/events/agent-words.ts +124 -0
- package/src/events/{cards.ts → requests.ts} +44 -44
- package/src/events/resume.test.ts +8 -1
- package/src/events/resume.ts +4 -0
- package/src/events/transcript.ts +51 -22
- package/src/events/watch-wake.test.ts +3 -2
- package/src/events/watch-wake.ts +4 -0
- package/src/ids/conversation-ids.test.ts +3 -3
- package/src/ids/hostnames.test.ts +1 -1
- package/src/ids/share-paths.test.ts +1 -1
- package/src/ids/tunnel-ids.test.ts +1 -1
- package/src/index.ts +10 -4
- package/src/models/agent-catalog.test.ts +1 -1
- package/src/models/capability-ledger.test.ts +1 -1
- package/src/models/model-offer.test.ts +9 -2
- package/src/models/model-offer.ts +10 -7
- package/src/models/model-order.test.ts +1 -1
- package/src/models/model-pins.test.ts +1 -1
- package/src/models/model-roles.test.ts +1 -1
- package/src/models/model-roles.ts +6 -12
- package/src/models/plan-pools.test.ts +1 -1
- package/src/models/provider-specs.test.ts +3 -3
- package/src/policy/batch-runs.test.ts +1 -1
- package/src/policy/command-classes.test.ts +37 -2
- package/src/policy/command-classes.ts +16 -0
- package/src/policy/credential-material.test.ts +6 -10
- package/src/policy/fence-paths.test.ts +1 -1
- package/src/policy/overlay-lint.test.ts +1 -1
- package/src/policy/owner-ticket.test.ts +1 -1
- package/src/policy/persona-home.test.ts +52 -0
- package/src/policy/persona-home.ts +39 -0
- package/src/policy/{card-status.ts → request-status.ts} +6 -6
- package/src/policy/safety-policy.test.ts +1 -1
- package/src/policy/safety-policy.ts +1 -0
- package/src/policy/search-globs.test.ts +1 -1
- package/src/policy/turned-away.ts +18 -0
- package/src/protocol/container-requirements.test.ts +1 -1
- package/src/protocol/ingress-contract.test.ts +8 -2
- package/src/protocol/ingress-protocol.test.ts +5 -4
- package/src/protocol/peer-dial.test.ts +111 -41
- package/src/protocol/peer-dial.ts +39 -4
- package/src/protocol/peer-mcp-server.test.ts +37 -7
- package/src/protocol/peer-mcp-server.ts +22 -1
- package/src/protocol/routes.test.ts +4 -2
- package/src/schemas/agent.ts +13 -0
- package/src/schemas/agents.ts +33 -0
- package/src/schemas/areas.test.ts +2 -8
- package/src/schemas/automations.ts +5 -5
- package/src/schemas/capabilities.ts +20 -9
- package/src/schemas/chat-route.ts +61 -0
- package/src/schemas/context-trim.test.ts +38 -0
- package/src/schemas/context-trim.ts +29 -0
- package/src/schemas/dev-rebuild-log.test.ts +1 -1
- package/src/schemas/devices.ts +25 -22
- package/src/schemas/extension-updates.ts +1 -1
- package/src/schemas/git/git.test.ts +11 -0
- package/src/schemas/git/git.ts +66 -0
- package/src/schemas/hosts.ts +15 -8
- package/src/schemas/issues.ts +1 -1
- package/src/schemas/metrics.ts +107 -0
- package/src/schemas/panels.ts +2 -2
- package/src/schemas/personas.ts +5 -28
- package/src/schemas/providers/plan-limits.ts +9 -0
- package/src/schemas/secrets.ts +1 -1
- package/src/schemas/settings.test.ts +1 -1
- package/src/schemas/settings.ts +9 -36
- package/src/schemas/shared.ts +6 -6
- package/src/schemas/system-prompt.ts +41 -0
- package/src/schemas/version-seam.test.ts +1 -1
- package/src/schemas/webext.ts +1 -1
- package/src/schemas/workflows.ts +4 -4
- package/src/state/contract-lock.test.ts +15 -11
- package/src/state/fix-attempt-plan.test.ts +10 -1
- package/src/state/fix-attempt-plan.ts +6 -2
- package/src/state/fix-stance.test.ts +10 -1
- package/src/state/fix-stance.ts +13 -0
- package/src/state/history-state.ts +17 -1
- package/src/state/runtime-state.test.ts +3 -9
- package/src/state/runtime-state.ts +0 -4
- package/src/state/versions.test.ts +1 -1
- package/src/state/workspace-state.test.ts +12 -7
- package/src/state/workspace-state.ts +4 -4
- package/src/text/archives.test.ts +1 -1
- package/src/text/documents.test.ts +1 -1
- package/src/text/documents.ts +2 -2
- package/src/text/embed.test.ts +17 -6
- package/src/text/emoji.test.ts +1 -1
- package/src/text/mentions.test.ts +20 -2
- package/src/text/mentions.ts +10 -1
- package/src/text/path-refs.test.ts +1 -1
- package/src/text/title.test.ts +1 -1
- package/src/text/transcript-fold.test.ts +183 -1
- package/src/text/transcript-fold.ts +146 -39
- package/src/text/whisper.test.ts +1 -1
- package/src/text/workflow-faults.test.ts +1 -1
- package/src/time/zone.test.ts +6 -6
- package/dist/contracts/host.contract.d.ts.map +0 -1
- package/dist/contracts/host.contract.js +0 -16
- package/dist/contracts/host.contract.js.map +0 -1
- package/dist/events/cards.d.ts.map +0 -1
- package/dist/events/cards.js.map +0 -1
- package/dist/policy/card-status.d.ts +0 -5
- package/dist/policy/card-status.d.ts.map +0 -1
- package/dist/policy/card-status.js.map +0 -1
- package/dist/schemas/model-route.d.ts +0 -27
- package/dist/schemas/model-route.d.ts.map +0 -1
- package/dist/schemas/model-route.js.map +0 -1
- package/src/schemas/model-route.ts +0 -43
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import {
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { AgentTurnSchema } from "../schemas/agent.js";
|
|
3
|
+
import { MENTION_LIMIT, mentionedPathTokens, mentionPaths } from "./mentions.js";
|
|
3
4
|
|
|
4
5
|
describe(`mentionPaths`, () => {
|
|
5
6
|
it(`extracts path-looking tokens, deduped, with trailing punctuation stripped`, () => {
|
|
@@ -13,6 +14,23 @@ describe(`mentionPaths`, () => {
|
|
|
13
14
|
it(`skips scoped package script prefixes in copied pnpm output`, () => {
|
|
14
15
|
expect(mentionPaths(`@intentic/iq-engine:test: failed\nsee @src/app.ts`)).toEqual([`src/app.ts`]);
|
|
15
16
|
});
|
|
17
|
+
|
|
18
|
+
// A pasted `ps` dump attached a file nobody chose and the turn died on it: curl's `@file` argument is a mention's
|
|
19
|
+
// exact shape.
|
|
20
|
+
it(`skips curl's @file argument and anything else outside the workspace`, () => {
|
|
21
|
+
expect(mentionPaths(`curl --data-binary @/tmp/probe/req.json localhost`)).toEqual([]);
|
|
22
|
+
expect(mentionPaths(`@~/notes.md @C:\\tmp\\out.log @../outside.bin @.. done`)).toEqual([]);
|
|
23
|
+
expect(mentionPaths(`curl -d @/tmp/req.json x, then read @src/app.ts`)).toEqual([`src/app.ts`]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// The wire takes MENTION_LIMIT paths: past it the schema would refuse the whole message, which is the failure a
|
|
27
|
+
// pasted log must not be able to cause.
|
|
28
|
+
it(`stops at the limit a turn can carry rather than overflowing it`, () => {
|
|
29
|
+
const pasted = Array.from({ length: MENTION_LIMIT + 5 }, (_, index) => `@src/file${index}.ts`).join(` `);
|
|
30
|
+
|
|
31
|
+
expect(mentionPaths(pasted)).toHaveLength(MENTION_LIMIT);
|
|
32
|
+
expect(AgentTurnSchema.safeParse({ prompt: pasted, mentions: mentionPaths(pasted) }).success).toBe(true);
|
|
33
|
+
});
|
|
16
34
|
});
|
|
17
35
|
|
|
18
36
|
describe(`mentionedPathTokens`, () => {
|
package/src/text/mentions.ts
CHANGED
|
@@ -18,4 +18,13 @@ export const mentionedPathTokens = (text: string): string[] => {
|
|
|
18
18
|
// Excludes a copied pnpm script prefix (`@scope/package:test:`), which shares the opening shape but is not a file;
|
|
19
19
|
// accepting it creates a phantom attachment.
|
|
20
20
|
const PACKAGE_SCRIPT = /^[^/]+\/[^/]+:[^/]+$/u;
|
|
21
|
-
|
|
21
|
+
// A mention names a workspace-relative path and nothing else: the picker inserts one from the workspace index, an
|
|
22
|
+
// upload mints one under the state dir. An absolute, home, drive-letter or climbing token is therefore never a
|
|
23
|
+
// mention, and curl's `@file` argument (`--data-binary @/tmp/req.json`) wears exactly that shape in pasted output.
|
|
24
|
+
const OUTSIDE_WORKSPACE = /^(?:[/~]|[A-Za-z]:[\\/]|\.\.(?:[/\\]|$))/u;
|
|
25
|
+
// How many a turn may carry; the tokenizer stops here rather than letting a pasted log's 21st token refuse the turn.
|
|
26
|
+
export const MENTION_LIMIT = 20;
|
|
27
|
+
export const mentionPaths = (text: string): string[] =>
|
|
28
|
+
mentionedPathTokens(text)
|
|
29
|
+
.filter((token) => !PACKAGE_SCRIPT.test(token) && !OUTSIDE_WORKSPACE.test(token))
|
|
30
|
+
.slice(0, MENTION_LIMIT);
|
package/src/text/title.test.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { STATE_DIR } from "@intentic/constants";
|
|
2
|
-
import { describe,
|
|
2
|
+
import { describe, it, expect } from "bun:test";
|
|
3
3
|
import type { AgentEvent } from "../events/agent-events.js";
|
|
4
4
|
import type { TranscriptPatch, TranscriptRow } from "../events/transcript.js";
|
|
5
5
|
import { watchWakePrompt } from "../events/watch-wake.js";
|
|
6
|
+
import { childReportPrompt, peerMessagePrompt } from "../events/agent-words.js";
|
|
6
7
|
import { applyTranscriptPatch, foldTurn, TranscriptFold, userRow } from "./transcript-fold.js";
|
|
7
8
|
|
|
8
9
|
// Epoch ms stamped on the opening user row's sentAt.
|
|
@@ -443,6 +444,37 @@ describe("patches", () => {
|
|
|
443
444
|
expect(fold.steerRows).toEqual([]);
|
|
444
445
|
});
|
|
445
446
|
|
|
447
|
+
// Another agent's words are drawn as theirs, never as the owner's.
|
|
448
|
+
it("writes a peer's message and a child's report into a live turn as notices, not as the owner's words", () => {
|
|
449
|
+
const fold = new TranscriptFold(openingOf("ship it"));
|
|
450
|
+
fold.apply({
|
|
451
|
+
kind: "steer",
|
|
452
|
+
text: peerMessagePrompt({ from: "sharp-shale-htw8", title: "Bun migration", message: "done" }),
|
|
453
|
+
sentAt: SENT_AT + 1,
|
|
454
|
+
voice: "agent",
|
|
455
|
+
});
|
|
456
|
+
fold.apply({
|
|
457
|
+
kind: "steer",
|
|
458
|
+
text: childReportPrompt({ child: "sub-x7", title: "Port the parser", failed: false, report: "ported", verification: undefined }),
|
|
459
|
+
sentAt: SENT_AT + 2,
|
|
460
|
+
voice: "sandbox",
|
|
461
|
+
});
|
|
462
|
+
expect(fold.rows.slice(1)).toMatchObject([
|
|
463
|
+
{ role: "notice", agentWords: { kind: "peer", from: "sharp-shale-htw8" } },
|
|
464
|
+
{ role: "notice", agentWords: { kind: "child", from: "sub-x7" } },
|
|
465
|
+
]);
|
|
466
|
+
expect(fold.steerRows).toEqual([]);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// An anchor from any other voice would shift every later person's anchor onto the wrong message.
|
|
470
|
+
it("anchors only a person's steer, even when another voice's words draw as a user row", () => {
|
|
471
|
+
const fold = new TranscriptFold(openingOf("ship it"));
|
|
472
|
+
fold.apply({ kind: "steer", text: "also cover the parser", sentAt: SENT_AT + 1, voice: "agent" });
|
|
473
|
+
fold.apply({ kind: "steer", text: "and the docs", sentAt: SENT_AT + 2 });
|
|
474
|
+
expect(fold.rows.map((row) => row.role)).toEqual(["user", "user", "user"]);
|
|
475
|
+
expect(fold.steerRows).toEqual([2]);
|
|
476
|
+
});
|
|
477
|
+
|
|
446
478
|
it("drop an empty bubble the turn opened and abandoned", () => {
|
|
447
479
|
const events: AgentEvent[] = [
|
|
448
480
|
{ kind: "todos", items: [] },
|
|
@@ -462,3 +494,153 @@ describe("patches", () => {
|
|
|
462
494
|
expect(fold.rows[1]?.text).toBe("ab");
|
|
463
495
|
});
|
|
464
496
|
});
|
|
497
|
+
|
|
498
|
+
// A refusal that ran nothing offers the message back to the composer — true where somebody typed one, false for a run
|
|
499
|
+
// that started itself, where there is no composer and no typed message.
|
|
500
|
+
describe(`a refusal that ran nothing`, () => {
|
|
501
|
+
const REFUSED = { kind: `error`, code: `context-window-too-small`, message: `This model accepts 16,384 tokens.` } as const satisfies AgentEvent;
|
|
502
|
+
|
|
503
|
+
it(`holds a turn somebody typed for another press`, () => {
|
|
504
|
+
const rows = foldOf(`land it`, [REFUSED]);
|
|
505
|
+
|
|
506
|
+
expect(rows.at(-1)?.text).toContain(`held for you to send again`);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it(`promises no resend for a turn that started itself`, () => {
|
|
510
|
+
const rows = foldOf(`land it`, [{ ...REFUSED, unattended: true }]);
|
|
511
|
+
|
|
512
|
+
expect(rows.at(-1)?.text).not.toContain(`send again`);
|
|
513
|
+
expect(rows.at(-1)?.text).toContain(`started on its own`);
|
|
514
|
+
// The provider's own sentence still leads, whichever clause follows it.
|
|
515
|
+
expect(rows.at(-1)?.text).toContain(`16,384 tokens`);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// The daemon lets a person's next send through once it has warned them, so the row says so and always offers it;
|
|
519
|
+
// the raise joins only when the hold carried a ceiling to size it by, which a stall does not.
|
|
520
|
+
describe(`on a sandbox short of memory`, () => {
|
|
521
|
+
const GIB = 1024 ** 3;
|
|
522
|
+
const lowMemory = {
|
|
523
|
+
kind: `error`,
|
|
524
|
+
code: `sandbox-memory-low`,
|
|
525
|
+
message: `Sandbox memory is low: 12.0 GiB resident + 4.0 GiB swapped, against 16.0 GiB.`,
|
|
526
|
+
memory: { limitBytes: 16 * GIB, residentBytes: 12 * GIB, swapBytes: 4 * GIB },
|
|
527
|
+
} as const satisfies AgentEvent;
|
|
528
|
+
|
|
529
|
+
it(`tells the reader that sending again starts it, and offers both presses when there is a ceiling to raise`, () => {
|
|
530
|
+
const row = foldOf(`land it`, [lowMemory]).at(-1);
|
|
531
|
+
expect(row?.text).toBe(`${lowMemory.message} Your message is held: send it again to start anyway.`);
|
|
532
|
+
expect(row?.noticeAction).toBe(`sandboxMemory`);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it(`offers only the send on a stall, which names no ceiling`, () => {
|
|
536
|
+
const { memory: _stalled, ...noReading } = lowMemory;
|
|
537
|
+
const row = foldOf(`land it`, [noReading]).at(-1);
|
|
538
|
+
expect(row?.noticeAction).toBe(`sendAnyway`);
|
|
539
|
+
expect(row?.text).toContain(`send it again to start anyway`);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it(`offers nothing on a background turn, which has no message to send`, () => {
|
|
543
|
+
const row = foldOf(`land it`, [{ ...lowMemory, unattended: true }]).at(-1);
|
|
544
|
+
expect(row?.noticeAction).toBeUndefined();
|
|
545
|
+
expect(row?.text).toContain(`started on its own`);
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// The other held refusals share the sentence, not the button: nothing about a dead credential is fixed by a cap.
|
|
550
|
+
it(`offers no press on the held refusals that a raise would not fix`, () => {
|
|
551
|
+
expect(foldOf(`land it`, [REFUSED]).at(-1)?.noticeAction).toBeUndefined();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// The bug this retraction exists for: the composer keeps the words for another press, so a bubble left standing
|
|
555
|
+
// here is the SAME message a second time, and a third, once per press against a gate that keeps refusing.
|
|
556
|
+
it(`takes the message back out, leaving the composer's copy as the only one`, () => {
|
|
557
|
+
const fold = new TranscriptFold(openingOf(`land it`));
|
|
558
|
+
const patches = fold.apply(REFUSED);
|
|
559
|
+
|
|
560
|
+
expect(fold.rows).toEqual([{ role: `notice`, text: expect.stringContaining(`held for you to send again`) }]);
|
|
561
|
+
expect(fold.ranNothing).toBe(true);
|
|
562
|
+
// Dropped ahead of the notice that stands in for it: a window applies patches in order, and an append first
|
|
563
|
+
// would have it renumber every row under an index that is about to move.
|
|
564
|
+
expect(patches).toEqual([
|
|
565
|
+
{ op: `drop`, index: 0 },
|
|
566
|
+
{ op: `append`, row: expect.objectContaining({ role: `notice` }) },
|
|
567
|
+
]);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Three presses is what the screenshot of this bug showed: one paragraph, four times, with a refusal between each.
|
|
571
|
+
it(`leaves nothing behind however many times the same message is refused`, () => {
|
|
572
|
+
const pressed = [0, 1, 2].map(() => foldOf(`land it`, [REFUSED]));
|
|
573
|
+
|
|
574
|
+
expect(pressed.flat().filter((row) => row.role === `user`)).toEqual([]);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it(`keeps the message of a run that started itself, which has no composer to repeat from`, () => {
|
|
578
|
+
const fold = new TranscriptFold(openingOf(`land it`));
|
|
579
|
+
fold.apply({ ...REFUSED, unattended: true });
|
|
580
|
+
|
|
581
|
+
expect(fold.rows[0]).toEqual(expect.objectContaining({ role: `user`, text: `land it` }));
|
|
582
|
+
expect(fold.ranNothing).toBe(false);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// A refusal is only a retraction where it refused the whole turn. One arriving after the agent has spoken ends a
|
|
586
|
+
// turn that happened, and everything it said is kept and recorded.
|
|
587
|
+
it(`keeps a turn the refusal interrupted rather than prevented`, () => {
|
|
588
|
+
const fold = new TranscriptFold(openingOf(`land it`));
|
|
589
|
+
fold.apply({ kind: `delta`, text: `on it` });
|
|
590
|
+
fold.apply(REFUSED);
|
|
591
|
+
|
|
592
|
+
expect(fold.rows.map((row) => row.role)).toEqual([`user`, `assistant`, `notice`]);
|
|
593
|
+
expect(fold.ranNothing).toBe(false);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// The sandbox, not a composer, keeps a turn it started itself (a fix press, a peer's message): the frame says so
|
|
597
|
+
// with `held`. The bug this exists for: such a refusal took the fix prompt out, and no queue anywhere held a copy.
|
|
598
|
+
describe(`of a turn the sandbox keeps`, () => {
|
|
599
|
+
const kept = { ...REFUSED, held: { ran: false } } as const satisfies AgentEvent;
|
|
600
|
+
|
|
601
|
+
it(`leaves the message where it was, and is recorded like any turn`, () => {
|
|
602
|
+
const fold = new TranscriptFold(openingOf(`land it`));
|
|
603
|
+
const patches = fold.apply(kept);
|
|
604
|
+
|
|
605
|
+
expect(fold.rows[0]).toEqual(expect.objectContaining({ role: `user`, text: `land it` }));
|
|
606
|
+
expect(fold.ranNothing).toBe(false);
|
|
607
|
+
expect(patches).toEqual([{ op: `append`, row: expect.objectContaining({ role: `notice` }) }]);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it(`offers to send it again, as the sandbox's to run rather than the composer's`, () => {
|
|
611
|
+
const row = foldOf(`land it`, [kept]).at(-1);
|
|
612
|
+
expect(row?.text).toBe(`${REFUSED.message} Nothing has run yet: the message above is kept here to send again once that is sorted.`);
|
|
613
|
+
expect(row).toMatchObject({ noticeAction: `sendAgain`, sandboxHeld: true });
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it(`offers the memory hold's own presses, anyway and the raise`, () => {
|
|
617
|
+
const GIB = 1024 ** 3;
|
|
618
|
+
const lowMemory = {
|
|
619
|
+
kind: `error`,
|
|
620
|
+
code: `sandbox-memory-low`,
|
|
621
|
+
message: `Sandbox memory is low: 15.9 GiB of 16.0 GiB used.`,
|
|
622
|
+
memory: { limitBytes: 16 * GIB, residentBytes: 15.9 * GIB, swapBytes: 0 },
|
|
623
|
+
held: { ran: false },
|
|
624
|
+
} as const satisfies AgentEvent;
|
|
625
|
+
const row = foldOf(`land it`, [lowMemory]).at(-1);
|
|
626
|
+
expect(row?.text).toBe(`${lowMemory.message} Nothing has run yet: the message above is kept here, and sending it anyway starts it.`);
|
|
627
|
+
expect(row).toMatchObject({ noticeAction: `sandboxMemory`, sandboxHeld: true });
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// Asked by whoever keeps the words before the frame is folded, so it has to agree with the fold's own reading.
|
|
632
|
+
describe(`turned away at the door`, () => {
|
|
633
|
+
it(`is a refusal before anything was said, with somebody watching`, () => {
|
|
634
|
+
const fold = new TranscriptFold(openingOf(`land it`));
|
|
635
|
+
expect(fold.turnedAway(REFUSED)).toBe(true);
|
|
636
|
+
expect(fold.turnedAway({ ...REFUSED, unattended: true })).toBe(false);
|
|
637
|
+
expect(fold.turnedAway({ kind: `error`, code: `rate_limit`, message: `Usage limit reached.` })).toBe(false);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it(`is not a refusal that ended a turn which had already spoken`, () => {
|
|
641
|
+
const fold = new TranscriptFold(openingOf(`land it`));
|
|
642
|
+
fold.apply({ kind: `delta`, text: `on it` });
|
|
643
|
+
expect(fold.turnedAway(REFUSED)).toBe(false);
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
});
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cancelledRequests, settledRequests } from "../policy/request-status.js";
|
|
2
2
|
import type { AgentEvent } from "../events/agent-events.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
REQUEST_FIELDS,
|
|
5
|
+
holdsRequest,
|
|
6
|
+
isAwaitingDecision,
|
|
7
|
+
type TranscriptRequests,
|
|
8
|
+
type TranscriptPatch,
|
|
9
|
+
type TranscriptRow,
|
|
10
|
+
type TranscriptSubagent,
|
|
11
|
+
type TranscriptTool,
|
|
12
|
+
} from "../events/transcript.js";
|
|
13
|
+
import { contextTrimLine } from "../schemas/context-trim.js";
|
|
14
|
+
import { turnedAwayCode } from "../policy/turned-away.js";
|
|
4
15
|
import { mentionedPathTokens } from "./mentions.js";
|
|
5
|
-
import {
|
|
16
|
+
import { unspokenPromptRow } from "../events/agent-words.js";
|
|
6
17
|
|
|
7
18
|
// Folds a turn's frames into rows once, live and for the settled record alike, so a reopened chat matches what was on
|
|
8
19
|
// screen. `tag` selects the stream read: undefined is the main turn, a tool-call id is the subagent it spawned; other
|
|
@@ -18,7 +29,8 @@ interface CardPlace {
|
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
// Strips undefined fields, so spreading a partial frame never overwrites a field an earlier frame already set.
|
|
21
|
-
const defined = <T extends object>(value: T): Partial<T> =>
|
|
32
|
+
const defined = <T extends object>(value: T): Partial<T> =>
|
|
33
|
+
Object.fromEntries(Object.entries(value).filter(([, field]) => field !== undefined)) as Partial<T>;
|
|
22
34
|
|
|
23
35
|
const cardOf = (event: Extract<AgentEvent, { kind: "tool_call" }>): TranscriptTool => ({
|
|
24
36
|
id: event.id,
|
|
@@ -30,6 +42,11 @@ const cardOf = (event: Extract<AgentEvent, { kind: "tool_call" }>): TranscriptTo
|
|
|
30
42
|
...(event.content !== undefined ? { content: event.content } : {}),
|
|
31
43
|
});
|
|
32
44
|
|
|
45
|
+
// Whether a just-retired prose bubble says exactly what the plan about to be drawn says, so the request can take its
|
|
46
|
+
// place instead of the same markdown appearing twice in a row.
|
|
47
|
+
const restates = (row: TranscriptRow | undefined, text: string): boolean =>
|
|
48
|
+
row?.role === "assistant" && !holdsRequest(row) && row.text.trim() !== "" && row.text.trim() === text.trim();
|
|
49
|
+
|
|
33
50
|
// Whether a bubble has any content: text, thinking, tools, todos, usage or a card; empty otherwise.
|
|
34
51
|
const empty = (row: TranscriptRow): boolean =>
|
|
35
52
|
row.text.length === 0 &&
|
|
@@ -37,7 +54,7 @@ const empty = (row: TranscriptRow): boolean =>
|
|
|
37
54
|
(row.tools?.length ?? 0) === 0 &&
|
|
38
55
|
(row.todos?.length ?? 0) === 0 &&
|
|
39
56
|
row.usage === undefined &&
|
|
40
|
-
!
|
|
57
|
+
!holdsRequest(row);
|
|
41
58
|
|
|
42
59
|
// Clause appended to the landed notice for a workspace dependency change, or empty when there is none. Reports the
|
|
43
60
|
// install as already started, or queued when other agents are still running; never as a request.
|
|
@@ -89,6 +106,46 @@ const landedRow = (event: Extract<AgentEvent, { kind: "landed" }>): TranscriptRo
|
|
|
89
106
|
};
|
|
90
107
|
};
|
|
91
108
|
|
|
109
|
+
// The clause after a refusal that ran nothing. A turn somebody typed is held in the composer for another press; a turn
|
|
110
|
+
// that started itself — an automation, a loop, a watch wake — has no typed message and nobody watching, so promising a
|
|
111
|
+
// resend there names a composer the reader is not looking at. Neither version promises a retry: whether one comes is
|
|
112
|
+
// the scheduler's decision, not this row's to state.
|
|
113
|
+
const undelivered = (unattended: boolean): string =>
|
|
114
|
+
unattended
|
|
115
|
+
? `Nothing ran, and nothing is held: this run started on its own, so there is no message waiting to be sent again.`
|
|
116
|
+
: `Your message was not delivered: it is held for you to send again.`;
|
|
117
|
+
|
|
118
|
+
// The press a memory hold offers: the raise rides along only when the hold carried a ceiling to size it by, since a
|
|
119
|
+
// stall names none and raising one would not clear it.
|
|
120
|
+
const memoryPress = (event: Extract<AgentEvent, { kind: "error" }>): "sendAnyway" | "sandboxMemory" =>
|
|
121
|
+
event.memory === undefined ? "sendAnyway" : "sandboxMemory";
|
|
122
|
+
|
|
123
|
+
// A memory hold is asked once and never again that spell (memory-admission.ts), so the next send always runs. A turn the
|
|
124
|
+
// sandbox started itself (a fix press, a peer's message, a re-run) was never in a composer: its message stays above, and
|
|
125
|
+
// the press asks the sandbox to run the turn it kept rather than sending a queue that holds nothing.
|
|
126
|
+
const heldRow = (event: Extract<AgentEvent, { kind: "error" }>): TranscriptRow => {
|
|
127
|
+
const unattended = event.unattended === true;
|
|
128
|
+
const memory = event.code === "sandbox-memory-low";
|
|
129
|
+
if (event.held !== undefined && !unattended) {
|
|
130
|
+
return {
|
|
131
|
+
role: "notice",
|
|
132
|
+
text: memory
|
|
133
|
+
? `${event.message} Nothing has run yet: the message above is kept here, and sending it anyway starts it.`
|
|
134
|
+
: `${event.message} Nothing has run yet: the message above is kept here to send again once that is sorted.`,
|
|
135
|
+
noticeAction: memory ? memoryPress(event) : "sendAgain",
|
|
136
|
+
sandboxHeld: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (!memory || unattended) {
|
|
140
|
+
return { role: "notice", text: `${event.message} ${undelivered(unattended)}` };
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
role: "notice",
|
|
144
|
+
text: `${event.message} Your message is held: send it again to start anyway.`,
|
|
145
|
+
noticeAction: memoryPress(event),
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
92
149
|
// Row for a turn-ending error: the provider's own message plus one clause on what happens next. The live wait itself is
|
|
93
150
|
// drawn by the chat, not stored here.
|
|
94
151
|
const errorRow = (event: Extract<AgentEvent, { kind: "error" }>): TranscriptRow => {
|
|
@@ -100,22 +157,21 @@ const errorRow = (event: Extract<AgentEvent, { kind: "error" }>): TranscriptRow
|
|
|
100
157
|
: { role: "notice", text: `${message} Retrying by itself: attempt ${event.outage.attempt} of ${event.outage.maxAttempts}.` };
|
|
101
158
|
case "claude-token-refused":
|
|
102
159
|
return event.autoResume === "scheduled"
|
|
103
|
-
? {
|
|
160
|
+
? {
|
|
161
|
+
role: "notice",
|
|
162
|
+
text: `${message} The credential is being renewed and this turn continues automatically.`,
|
|
163
|
+
noticeWait: "credentialRenewal",
|
|
164
|
+
}
|
|
104
165
|
: { role: "notice", text: `${message} Reconnect the account to pick this conversation back up.` };
|
|
105
166
|
case "rate_limit":
|
|
106
167
|
return { role: "notice", text: message };
|
|
107
|
-
// Refused before the model saw it; the composer holds the message so the user can resend it.
|
|
108
|
-
case "claude-reauth":
|
|
109
|
-
case "unknown-command":
|
|
110
|
-
case "context-window-too-small":
|
|
111
|
-
case "sandbox-memory-low":
|
|
112
|
-
case "trial-unavailable":
|
|
113
|
-
case "trial-model-unavailable":
|
|
114
|
-
case "trial-exhausted":
|
|
115
|
-
return { role: "notice", text: `${message} Your message was not delivered: it is held for you to send again.` };
|
|
116
168
|
default:
|
|
117
|
-
|
|
169
|
+
break;
|
|
118
170
|
}
|
|
171
|
+
if (!turnedAwayCode(code)) {
|
|
172
|
+
return { role: "notice", text: message };
|
|
173
|
+
}
|
|
174
|
+
return heldRow(event);
|
|
119
175
|
};
|
|
120
176
|
|
|
121
177
|
// The turn's opening user row: text, timestamp, attachments, and whatever the daemon later stamps onto it (checkpoint,
|
|
@@ -131,13 +187,17 @@ export class TranscriptFold {
|
|
|
131
187
|
readonly rows: TranscriptRow[] = [];
|
|
132
188
|
// Row index of each user steer, in order; the daemon anchors rewind state to these once the turn settles.
|
|
133
189
|
readonly steerRows: number[] = [];
|
|
190
|
+
// Whether anything was steered in, whoever spoke: a retract may not take back an opening something followed.
|
|
191
|
+
private steeredIn = false;
|
|
134
192
|
// Index of the open assistant bubble; always the last row, since every other row kind closes it first.
|
|
135
193
|
private bubble: number | undefined;
|
|
136
194
|
private readonly cards = new Map<string, CardPlace>();
|
|
137
195
|
// requestId to the row holding its card, for frames landing on it later (a reply, a late sentence, a receipt).
|
|
138
196
|
private readonly parked = new Map<string, number>();
|
|
139
|
-
// The turn's opening user row, where the checkpoint and daemon notes land.
|
|
140
|
-
private
|
|
197
|
+
// The turn's opening user row, where the checkpoint and daemon notes land; cleared once `retract` takes it back.
|
|
198
|
+
private opener: number | undefined;
|
|
199
|
+
// Set by `retract`: this turn was refused before it ran, and its message went back to the composer.
|
|
200
|
+
private unrun = false;
|
|
141
201
|
|
|
142
202
|
constructor(
|
|
143
203
|
opening: readonly TranscriptRow[],
|
|
@@ -246,6 +306,10 @@ export class TranscriptFold {
|
|
|
246
306
|
case "preamble":
|
|
247
307
|
// Collapses the daemon's preamble notes onto the user row; an empty note list is not a disclosure.
|
|
248
308
|
return event.notes.length === 0 ? [] : this.stampOpener((row) => (row.notes = [...event.notes]));
|
|
309
|
+
case "context_trim":
|
|
310
|
+
// A row of its own rather than a stamp on the message: what was left out is not part of what was sent,
|
|
311
|
+
// and the fold beside the message only ever lists notes that actually rode.
|
|
312
|
+
return this.pushRow({ role: "notice", text: contextTrimLine(event) });
|
|
249
313
|
case "worktree":
|
|
250
314
|
return event.sync === undefined ? [] : this.pushRow({ role: "notice", text: syncLine(event.sync) });
|
|
251
315
|
case "landed":
|
|
@@ -254,17 +318,13 @@ export class TranscriptFold {
|
|
|
254
318
|
return this.pushRow({ role: "notice", text: `Context compacted to free up space.` });
|
|
255
319
|
case "error":
|
|
256
320
|
// Keeps a refusal (no prose from the provider) from reading as a session that ended mid-question.
|
|
257
|
-
|
|
321
|
+
// A refusal that ran nothing takes its message back out ahead of the notice standing in for it.
|
|
322
|
+
return [...this.retract(event), ...this.pushRow(errorRow(event))];
|
|
258
323
|
case "plan": {
|
|
259
324
|
// Folds a plan into an identical retired prose bubble instead of drawing the same markdown twice.
|
|
260
325
|
const adjacent = this.rows.at(-1);
|
|
261
|
-
const consumes =
|
|
262
|
-
|
|
263
|
-
adjacent?.role === "assistant" &&
|
|
264
|
-
!holdsCard(adjacent) &&
|
|
265
|
-
adjacent.text.trim() !== "" &&
|
|
266
|
-
adjacent.text.trim() === event.text.trim();
|
|
267
|
-
if (consumes) {
|
|
326
|
+
const consumes = this.bubble === undefined && restates(adjacent, event.text);
|
|
327
|
+
if (consumes && adjacent !== undefined) {
|
|
268
328
|
adjacent.text = "";
|
|
269
329
|
}
|
|
270
330
|
return this.park(
|
|
@@ -309,7 +369,7 @@ export class TranscriptFold {
|
|
|
309
369
|
return this.park(event.requestId, { credentialOffer: { requestId: event.requestId, offer: event.offer, status: "pending" } });
|
|
310
370
|
case "resolved":
|
|
311
371
|
// Releases the card; the answering window already froze it locally, so this is a no-op there.
|
|
312
|
-
return this.patchParked(event.requestId, (row) => Object.assign(row,
|
|
372
|
+
return this.patchParked(event.requestId, (row) => Object.assign(row, settledRequests(row, event.reply)));
|
|
313
373
|
case "capability_outcome":
|
|
314
374
|
return this.patchParked(event.requestId, (row) => {
|
|
315
375
|
if (row.capabilityOffer !== undefined) {
|
|
@@ -353,14 +413,12 @@ export class TranscriptFold {
|
|
|
353
413
|
}
|
|
354
414
|
}
|
|
355
415
|
|
|
356
|
-
/**
|
|
357
|
-
* A message pushed into a turn already running. A condition watch's wake arrives this way too, and it is the
|
|
358
|
-
* daemon's own words: it becomes a notice, and never an anchor the rewind can return a person to.
|
|
359
|
-
*/
|
|
416
|
+
/** A message pushed into a running turn; only a person's own message is a rewind anchor. */
|
|
360
417
|
private steered(event: Extract<AgentEvent, { kind: "steer" }>): TranscriptPatch[] {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
418
|
+
this.steeredIn = true;
|
|
419
|
+
const unspoken = unspokenPromptRow(event.text);
|
|
420
|
+
if (unspoken !== undefined) {
|
|
421
|
+
return this.pushRow(unspoken);
|
|
364
422
|
}
|
|
365
423
|
// A steer also closes the bubble, or the next answer would print over it mid-call.
|
|
366
424
|
const patches = this.pushRow({
|
|
@@ -369,7 +427,10 @@ export class TranscriptFold {
|
|
|
369
427
|
sentAt: event.sentAt,
|
|
370
428
|
...(event.attachments === undefined ? {} : { attachments: [...event.attachments] }),
|
|
371
429
|
});
|
|
372
|
-
|
|
430
|
+
// Anchors pair by position with the checkpoints only a person's steer reserves.
|
|
431
|
+
if (event.voice === undefined) {
|
|
432
|
+
this.steerRows.push(this.rows.length - 1);
|
|
433
|
+
}
|
|
373
434
|
return patches;
|
|
374
435
|
}
|
|
375
436
|
|
|
@@ -378,6 +439,46 @@ export class TranscriptFold {
|
|
|
378
439
|
return this.pushRow(row);
|
|
379
440
|
}
|
|
380
441
|
|
|
442
|
+
/**
|
|
443
|
+
* Whether the turn ran nothing: refused before the model saw it, message handed back; never recorded (turn-runs).
|
|
444
|
+
* Re-derived rather than latched, so a stream that spoke after its refusal still keeps what it said.
|
|
445
|
+
*/
|
|
446
|
+
get ranNothing(): boolean {
|
|
447
|
+
return this.unrun && !this.rows.some((row) => row.role === "assistant");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Whether this frame turns the whole turn away at the door: a refusal before the model saw a word, with somebody
|
|
452
|
+
* watching and nothing yet said or steered in. Asked before the frame is applied, by whoever keeps the words.
|
|
453
|
+
*/
|
|
454
|
+
turnedAway(event: AgentEvent): boolean {
|
|
455
|
+
return (
|
|
456
|
+
event.kind === "error" &&
|
|
457
|
+
turnedAwayCode(event.code) &&
|
|
458
|
+
event.unattended !== true &&
|
|
459
|
+
!this.rows.some((row) => row.role === "assistant") &&
|
|
460
|
+
!this.steeredIn
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Attended refusals only: the composer holds those words for another press, so a bubble left standing repeats them
|
|
465
|
+
// once per press. Splicing is safe because nothing ran — no open bubble, no parked card, no steer above it. A turn
|
|
466
|
+
// the sandbox kept (`held`) keeps its message and is recorded: no composer anywhere holds a copy of it.
|
|
467
|
+
private retract(event: Extract<AgentEvent, { kind: "error" }>): TranscriptPatch[] {
|
|
468
|
+
if (!this.turnedAway(event) || event.held !== undefined) {
|
|
469
|
+
return [];
|
|
470
|
+
}
|
|
471
|
+
this.unrun = true;
|
|
472
|
+
const opener = this.opener;
|
|
473
|
+
// An opening the daemon wrote (a watch wake, a resume disclosure) has no bubble to take back, and is still unrun.
|
|
474
|
+
if (opener === undefined) {
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
this.rows.splice(opener, 1);
|
|
478
|
+
this.opener = undefined;
|
|
479
|
+
return [{ op: "drop", index: opener }];
|
|
480
|
+
}
|
|
481
|
+
|
|
381
482
|
/**
|
|
382
483
|
* Ends the turn: closes the open bubble, freezes every still-pending card as nobody's decision, and notes a user
|
|
383
484
|
* stop.
|
|
@@ -386,7 +487,7 @@ export class TranscriptFold {
|
|
|
386
487
|
const patches = this.closeBubble();
|
|
387
488
|
for (const [index, row] of this.rows.entries()) {
|
|
388
489
|
if (row.role === "assistant" && isAwaitingDecision(row)) {
|
|
389
|
-
Object.assign(row,
|
|
490
|
+
Object.assign(row, cancelledRequests(row));
|
|
390
491
|
patches.push(this.replace(index));
|
|
391
492
|
}
|
|
392
493
|
}
|
|
@@ -453,7 +554,7 @@ export class TranscriptFold {
|
|
|
453
554
|
|
|
454
555
|
// A card takes the open bubble and closes it; `into` reuses an existing row (a plan's own prose) instead of opening
|
|
455
556
|
// a new one.
|
|
456
|
-
private park(requestId: string, cards:
|
|
557
|
+
private park(requestId: string, cards: TranscriptRequests, into?: number): TranscriptPatch[] {
|
|
457
558
|
const [index, opened] = into === undefined ? this.open() : [into, []];
|
|
458
559
|
Object.assign(this.rows[index]!, cards);
|
|
459
560
|
this.bubble = undefined;
|
|
@@ -493,7 +594,12 @@ export class TranscriptFold {
|
|
|
493
594
|
}
|
|
494
595
|
|
|
495
596
|
/** Folds a whole turn at once: opening rows, every frame, and how it ended; what a settled turn reads back as. */
|
|
496
|
-
export const foldTurn = (
|
|
597
|
+
export const foldTurn = (
|
|
598
|
+
opening: readonly TranscriptRow[],
|
|
599
|
+
events: readonly AgentEvent[],
|
|
600
|
+
ending: TurnEnding = "settled",
|
|
601
|
+
tag?: string,
|
|
602
|
+
): TranscriptRow[] => {
|
|
497
603
|
const fold = new TranscriptFold(opening, tag);
|
|
498
604
|
for (const event of events) {
|
|
499
605
|
fold.apply(event);
|
|
@@ -561,4 +667,5 @@ export const mapTool = (tools: readonly TranscriptTool[], id: string, fn: (tool:
|
|
|
561
667
|
};
|
|
562
668
|
|
|
563
669
|
// Which row fields are cards, exported for readers that count rows by them.
|
|
564
|
-
export const cardFieldsOf = (row: TranscriptRow):
|
|
670
|
+
export const cardFieldsOf = (row: TranscriptRow): TranscriptRequests =>
|
|
671
|
+
Object.fromEntries(REQUEST_FIELDS.flatMap((field) => (row[field] === undefined ? [] : [[field, row[field]]])));
|
package/src/text/whisper.test.ts
CHANGED
package/src/time/zone.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe,
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
2
|
import { asCivilDay, asZone, civilDayIn, cronOptions, isZone, sameClock, utcDayOf, UTC, zoneLabel } from "./zone.js";
|
|
3
3
|
import type { Zone } from "./zone.js";
|
|
4
4
|
|
|
@@ -28,7 +28,7 @@ describe(`isZone`, () => {
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
test(`asZone passes real ids through and swallows the rest`, () => {
|
|
31
|
-
expect(asZone(`America/New_York`)).toBe(`America/New_York`);
|
|
31
|
+
expect<string | undefined>(asZone(`America/New_York`)).toBe(`America/New_York`);
|
|
32
32
|
expect(asZone(`Mars/Olympus`)).toBeUndefined();
|
|
33
33
|
expect(asZone(undefined)).toBeUndefined();
|
|
34
34
|
});
|
|
@@ -39,9 +39,9 @@ describe(`civilDayIn`, () => {
|
|
|
39
39
|
test(`an instant falls on different days depending on the zone asked`, () => {
|
|
40
40
|
// 2026-09-20T23:30Z: already the 21st in Warsaw, still the 20th in UTC and New York.
|
|
41
41
|
const lateEvening = Date.UTC(2026, 8, 20, 23, 30);
|
|
42
|
-
expect(civilDayIn(lateEvening, WARSAW)).toBe(`2026-09-21`);
|
|
43
|
-
expect(civilDayIn(lateEvening, UTC)).toBe(`2026-09-20`);
|
|
44
|
-
expect(civilDayIn(lateEvening, NEW_YORK)).toBe(`2026-09-20`);
|
|
42
|
+
expect<string>(civilDayIn(lateEvening, WARSAW)).toBe(`2026-09-21`);
|
|
43
|
+
expect<string>(civilDayIn(lateEvening, UTC)).toBe(`2026-09-20`);
|
|
44
|
+
expect<string>(civilDayIn(lateEvening, NEW_YORK)).toBe(`2026-09-20`);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
test(`utcDayOf is civilDayIn UTC, spelled so every UTC bucket is greppable`, () => {
|
|
@@ -49,7 +49,7 @@ describe(`civilDayIn`, () => {
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
test(`asCivilDay takes the shape and nothing else`, () => {
|
|
52
|
-
expect(asCivilDay(`2026-09-20`)).toBe(`2026-09-20`);
|
|
52
|
+
expect<string | undefined>(asCivilDay(`2026-09-20`)).toBe(`2026-09-20`);
|
|
53
53
|
expect(asCivilDay(`2026-09-20T00:00:00Z`)).toBeUndefined();
|
|
54
54
|
expect(asCivilDay(`20/09/2026`)).toBeUndefined();
|
|
55
55
|
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"host.contract.d.ts","sourceRoot":"","sources":["../../src/contracts/host.contract.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AASxB,eAAO,MAAM,YAAY;IAErB,QAAQ;;;;;;;;;;;;;;;;IAER,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAET,IAAI;;;IAEJ,GAAG;IAEH,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAEd,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CACf,CAAC"}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { oc } from "@orpc/contract";
|
|
2
|
-
import { streamOf } from "../protocol/routes.js";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { DeviceAgentFlowSchema, DeviceFlowLineSchema, DeviceSandboxFlowSchema } from "../schemas/devices.js";
|
|
5
|
-
import { HostScopesSchema } from "../schemas/capabilities.js";
|
|
6
|
-
import { HostFactsSchema } from "../schemas/hosts.js";
|
|
7
|
-
import { OkSchema } from "../schemas/shared.js";
|
|
8
|
-
export const hostContract = {
|
|
9
|
-
describe: oc.output(HostFactsSchema),
|
|
10
|
-
setScopes: oc.input(HostScopesSchema).output(OkSchema),
|
|
11
|
-
ping: oc.output(OkSchema),
|
|
12
|
-
mcp: oc.input(z.unknown()).output(z.unknown()),
|
|
13
|
-
runSandboxFlow: oc.input(DeviceSandboxFlowSchema).output(streamOf(DeviceFlowLineSchema)),
|
|
14
|
-
runAgentFlow: oc.input(DeviceAgentFlowSchema).output(streamOf(DeviceFlowLineSchema)),
|
|
15
|
-
};
|
|
16
|
-
//# sourceMappingURL=host.contract.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"host.contract.js","sourceRoot":"","sources":["../../src/contracts/host.contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAC7G,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAKhD,MAAM,CAAC,MAAM,YAAY,GAAG;IAExB,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC;IAEpC,SAAS,EAAE,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC;IAEtD,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC;IAEzB,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;IAE9C,cAAc,EAAE,EAAE,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAC;IAExF,YAAY,EAAE,EAAE,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAC;CACvF,CAAC"}
|