@gajae-code/coding-agent 0.16.0 → 0.16.3
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/CHANGELOG.md +52 -1
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/model-registry.d.ts +16 -0
- package/dist/types/config/settings-schema.d.ts +17 -7
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -1
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +17 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +3 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/rows.d.ts +15 -2
- package/dist/types/sdk/cli/session-cli.d.ts +2 -1
- package/dist/types/sdk/host/events.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +18 -17
- package/dist/types/sdk/lifecycle-session.d.ts +1 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +4 -0
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +47 -7
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +14 -2
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +12 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/index.d.ts +4 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +12 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +7 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/update-cli.ts +63 -12
- package/src/commands/sdk.ts +22 -7
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/model-preset-registry.ts +177 -18
- package/src/config/model-registry.ts +268 -42
- package/src/config/settings-schema.ts +19 -7
- package/src/config/settings.ts +46 -15
- package/src/coordinator-mcp/projection-scan.ts +6 -6
- package/src/coordinator-mcp/server.ts +21 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/session-state-lock.ts +156 -32
- package/src/gjc-runtime/session-state-sidecar.ts +10 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +34 -19
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/selector-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +6 -1
- package/src/sdk/broker/broker.ts +10 -1
- package/src/sdk/broker/ensure.ts +76 -3
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +254 -34
- package/src/sdk/broker/spawn-substrate.ts +45 -2
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +17 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +118 -24
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +33 -3
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/rows.ts +75 -7
- package/src/sdk/cli/session-cli.ts +68 -35
- package/src/sdk/host/control/dispatch.ts +2 -2
- package/src/sdk/host/events.ts +26 -2
- package/src/sdk/host/host.ts +6 -1
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +497 -31
- package/src/sdk/host/session-runtime.ts +387 -54
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle-session.ts +7 -2
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +54 -3
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +210 -94
- package/src/sdk/session.ts +59 -41
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1033 -260
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/session-manager.ts +517 -77
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +158 -33
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/index.ts +4 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +86 -1
|
@@ -38,10 +38,13 @@ import { SESSION_REQUEST_TIMEOUT_MS } from "../session-reconnect";
|
|
|
38
38
|
import {
|
|
39
39
|
type SdkCheckpointRecordV1,
|
|
40
40
|
type SdkRetentionGapV1,
|
|
41
|
+
type SdkRingPositionV1,
|
|
41
42
|
type SdkSessionRowV1,
|
|
42
43
|
type SdkTailItemV1,
|
|
43
44
|
SESSION_ROWS_VERSION,
|
|
44
45
|
stripSecretFields,
|
|
46
|
+
TailRevisionBuffer,
|
|
47
|
+
tailItemKey,
|
|
45
48
|
toCheckpointRecordV1,
|
|
46
49
|
toRetentionGapV1,
|
|
47
50
|
toSessionRowV1,
|
|
@@ -613,14 +616,15 @@ async function workspaceIdentity(target: string): Promise<WorkspaceIdentity> {
|
|
|
613
616
|
/**
|
|
614
617
|
* Resolves the list selection from `--repo` (default: process cwd) under the
|
|
615
618
|
* effective scope. Outside Git, `repo`/`worktree` fail typed and actionable —
|
|
616
|
-
* they never broaden to `all`; `cwd` remains an exact canonical match
|
|
619
|
+
* they never broaden to `all`; `cwd` remains an exact canonical match, and
|
|
620
|
+
* `all` ignores the selection entirely so it works outside any checkout.
|
|
617
621
|
*/
|
|
618
622
|
export async function resolveSessionListSelection(
|
|
619
623
|
scope: SdkSessionListScope,
|
|
620
624
|
repoArg: string | undefined,
|
|
621
625
|
): Promise<SdkSessionListSelection> {
|
|
622
626
|
const selection = await workspaceIdentity(repoArg ?? process.cwd());
|
|
623
|
-
if (scope !== "cwd" && !selection.repoRoot)
|
|
627
|
+
if (scope !== "cwd" && scope !== "all" && !selection.repoRoot)
|
|
624
628
|
throw new SdkSessionCliError(
|
|
625
629
|
"not_a_repository",
|
|
626
630
|
`--scope ${scope} requires a Git repository, but "${selection.canonicalPath}" is outside any Git checkout. ` +
|
|
@@ -969,11 +973,7 @@ function extractCheckpoint(response: unknown): CheckpointExtraction {
|
|
|
969
973
|
if (gap !== undefined) return { gap };
|
|
970
974
|
const record = toCheckpointRecordV1(result.checkpoint ?? result);
|
|
971
975
|
const cursor =
|
|
972
|
-
typeof result.
|
|
973
|
-
? result.cursor
|
|
974
|
-
: typeof result.checkpointToken === "string" && result.checkpointToken
|
|
975
|
-
? result.checkpointToken
|
|
976
|
-
: undefined;
|
|
976
|
+
typeof result.checkpointToken === "string" && result.checkpointToken ? result.checkpointToken : undefined;
|
|
977
977
|
return { record, cursor };
|
|
978
978
|
}
|
|
979
979
|
|
|
@@ -988,13 +988,6 @@ function extractTranscriptPage(response: unknown): { items: unknown[]; complete:
|
|
|
988
988
|
};
|
|
989
989
|
}
|
|
990
990
|
|
|
991
|
-
function tailItemKey(item: SdkTailItemV1): string {
|
|
992
|
-
if (item.generation !== undefined && item.seq !== undefined)
|
|
993
|
-
return `${item.kind}\u0000${item.generation}\u0000${item.seq}`;
|
|
994
|
-
if (item.id !== undefined) return `${item.kind}\u0000${item.id}`;
|
|
995
|
-
return `${item.kind}\u0000${JSON.stringify(item.payload)}`;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
991
|
function mergeTailItems(
|
|
999
992
|
target: SdkTailItemV1[],
|
|
1000
993
|
seen: Set<string>,
|
|
@@ -1009,8 +1002,9 @@ function mergeTailItems(
|
|
|
1009
1002
|
}
|
|
1010
1003
|
}
|
|
1011
1004
|
|
|
1012
|
-
/** Canonical ring position of an event item, when the host stated one. */
|
|
1013
|
-
type TailSeqKey =
|
|
1005
|
+
/** Canonical session-wide ring position of an event item, when the host stated one. */
|
|
1006
|
+
type TailSeqKey = SdkRingPositionV1;
|
|
1007
|
+
type TailLocalPosition = Pick<SdkRingPositionV1, "generation" | "seq">;
|
|
1014
1008
|
|
|
1015
1009
|
function isPositionCoordinate(value: unknown): value is number {
|
|
1016
1010
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
@@ -1060,7 +1054,7 @@ function classifyRawEventPosition(event: unknown): "unpositioned" | "malformed"
|
|
|
1060
1054
|
* it is never silently downgraded to unpositioned. Claiming neither coordinate
|
|
1061
1055
|
* is a genuine unpositioned event.
|
|
1062
1056
|
*/
|
|
1063
|
-
function classifyTailPosition(item: SdkTailItemV1):
|
|
1057
|
+
function classifyTailPosition(item: SdkTailItemV1): TailLocalPosition | "unpositioned" | "malformed" {
|
|
1064
1058
|
if (item.generation === undefined && item.seq === undefined) return "unpositioned";
|
|
1065
1059
|
if (!isPositionCoordinate(item.generation) || !isPositionCoordinate(item.seq)) return "malformed";
|
|
1066
1060
|
return { generation: item.generation, seq: item.seq };
|
|
@@ -1068,10 +1062,13 @@ function classifyTailPosition(item: SdkTailItemV1): TailSeqKey | "unpositioned"
|
|
|
1068
1062
|
|
|
1069
1063
|
function tailSeqKey(item: SdkTailItemV1): TailSeqKey | undefined {
|
|
1070
1064
|
const position = classifyTailPosition(item);
|
|
1071
|
-
return typeof position === "string"
|
|
1065
|
+
return typeof position === "string" || !isPositionCoordinate(item.revision)
|
|
1066
|
+
? undefined
|
|
1067
|
+
: { revision: item.revision, ...position };
|
|
1072
1068
|
}
|
|
1073
1069
|
|
|
1074
1070
|
function compareTailSeqKeys(left: TailSeqKey, right: TailSeqKey): number {
|
|
1071
|
+
if (left.revision !== right.revision) return left.revision - right.revision;
|
|
1075
1072
|
return left.generation !== right.generation ? left.generation - right.generation : left.seq - right.seq;
|
|
1076
1073
|
}
|
|
1077
1074
|
|
|
@@ -1115,10 +1112,12 @@ function mergeEventTailItems(
|
|
|
1115
1112
|
target.unpositioned.push(item);
|
|
1116
1113
|
continue;
|
|
1117
1114
|
}
|
|
1115
|
+
const orderedPosition = tailSeqKey(item);
|
|
1116
|
+
if (orderedPosition === undefined) throw new Error("A positioned tail item has no authoritative revision.");
|
|
1118
1117
|
let index = target.positioned.length;
|
|
1119
1118
|
for (; index > 0; index--) {
|
|
1120
1119
|
const previous = tailSeqKey(target.positioned[index - 1]!);
|
|
1121
|
-
if (previous === undefined || compareTailSeqKeys(previous,
|
|
1120
|
+
if (previous === undefined || compareTailSeqKeys(previous, orderedPosition) <= 0) break;
|
|
1122
1121
|
}
|
|
1123
1122
|
target.positioned.splice(index, 0, item);
|
|
1124
1123
|
}
|
|
@@ -1150,12 +1149,6 @@ function eventGapToRetentionGap(
|
|
|
1150
1149
|
return undefined;
|
|
1151
1150
|
}
|
|
1152
1151
|
|
|
1153
|
-
function publicationSequence(publicationId: string): number | undefined {
|
|
1154
|
-
const parts = publicationId.split(":");
|
|
1155
|
-
const value = Number(parts.at(-1));
|
|
1156
|
-
return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
1152
|
function tailItemFromRouterFrame(frame: SessionRouterFrame): SdkTailItemV1 | undefined {
|
|
1160
1153
|
if (frame.name === undefined) return undefined;
|
|
1161
1154
|
// The replay response is an out-of-band control frame, not an event-ring
|
|
@@ -1163,18 +1156,18 @@ function tailItemFromRouterFrame(frame: SessionRouterFrame): SdkTailItemV1 | und
|
|
|
1163
1156
|
// intentionally has no event `seq`; projecting it as a tail item would make
|
|
1164
1157
|
// the strict event-position validator reject every otherwise valid tail.
|
|
1165
1158
|
if (frame.name === "event_replay_result") return undefined;
|
|
1166
|
-
const seq = frame.
|
|
1159
|
+
const seq = frame.seq;
|
|
1167
1160
|
return toTailItemV1(
|
|
1168
1161
|
{
|
|
1169
1162
|
kind: frame.name,
|
|
1170
|
-
...(frame.
|
|
1171
|
-
...(seq === undefined ? {} : { seq }),
|
|
1163
|
+
...(frame.revision === undefined ? {} : { revision: frame.revision }),
|
|
1164
|
+
...(seq === undefined ? {} : { generation: frame.generation, seq }),
|
|
1172
1165
|
payload: frame.body,
|
|
1173
1166
|
},
|
|
1174
1167
|
{
|
|
1175
1168
|
kind: frame.name,
|
|
1176
|
-
...(frame.
|
|
1177
|
-
...(seq === undefined ? {} : { seq }),
|
|
1169
|
+
...(frame.revision === undefined ? {} : { revision: frame.revision }),
|
|
1170
|
+
...(seq === undefined ? {} : { generation: frame.generation, seq }),
|
|
1178
1171
|
},
|
|
1179
1172
|
);
|
|
1180
1173
|
}
|
|
@@ -1388,7 +1381,9 @@ async function offlineTailReplay(
|
|
|
1388
1381
|
version: SESSION_ROWS_VERSION,
|
|
1389
1382
|
source: "offline",
|
|
1390
1383
|
session: row,
|
|
1391
|
-
items: entries.map((entry, index) =>
|
|
1384
|
+
items: entries.map((entry, index) =>
|
|
1385
|
+
toTailItemV1(entry, { kind: "transcript", revision: index + 1, seq: index }),
|
|
1386
|
+
),
|
|
1392
1387
|
terminal: true,
|
|
1393
1388
|
},
|
|
1394
1389
|
};
|
|
@@ -1415,6 +1410,7 @@ async function runLiveTail(
|
|
|
1415
1410
|
// evidence.
|
|
1416
1411
|
const seenTranscript = new Set<string>();
|
|
1417
1412
|
const seenEvents = new Set<string>();
|
|
1413
|
+
const liveRevisionBuffer = new TailRevisionBuffer();
|
|
1418
1414
|
let checkpoint: SdkCheckpointRecordV1 | undefined;
|
|
1419
1415
|
let gap: SdkRetentionGapV1 | undefined;
|
|
1420
1416
|
let liveReason: TailExitReason | undefined;
|
|
@@ -1454,7 +1450,7 @@ async function runLiveTail(
|
|
|
1454
1450
|
// close at a contested position win instead of failing closed, and
|
|
1455
1451
|
// checking it after the monotonic guard would silently skip a
|
|
1456
1452
|
// conflicting kind at an already-decided position.
|
|
1457
|
-
const positionKey = `${position.generation}\u0000${position.seq}`;
|
|
1453
|
+
const positionKey = `${position.revision}\u0000${position.generation}\u0000${position.seq}`;
|
|
1458
1454
|
const claimed = claimedLifecycleKinds.get(positionKey);
|
|
1459
1455
|
if (claimed !== undefined && claimed !== item.kind) {
|
|
1460
1456
|
malformed ??= new SdkSessionCliError(
|
|
@@ -1496,7 +1492,17 @@ async function runLiveTail(
|
|
|
1496
1492
|
if (attachment.sessionId !== sessionId) return;
|
|
1497
1493
|
const item = tailItemFromRouterFrame(frame);
|
|
1498
1494
|
if (!item) return;
|
|
1499
|
-
|
|
1495
|
+
try {
|
|
1496
|
+
applyLifecycle(mergeEventTailItems(eventItems, seenEvents, liveRevisionBuffer.push(item), include));
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
malformed ??= new SdkSessionCliError(
|
|
1499
|
+
"protocol_error",
|
|
1500
|
+
error instanceof Error ? error.message : "A live tail item had no authoritative revision.",
|
|
1501
|
+
1,
|
|
1502
|
+
{ sessionId, kind: item.kind, generation: item.generation, seq: item.seq },
|
|
1503
|
+
);
|
|
1504
|
+
rejectLive?.(malformed);
|
|
1505
|
+
}
|
|
1500
1506
|
};
|
|
1501
1507
|
|
|
1502
1508
|
return await withRouter(
|
|
@@ -1518,6 +1524,10 @@ async function runLiveTail(
|
|
|
1518
1524
|
const extraction = extractCheckpoint(checkpointResponse);
|
|
1519
1525
|
checkpoint = extraction.record;
|
|
1520
1526
|
gap = extraction.gap;
|
|
1527
|
+
if (checkpoint !== undefined)
|
|
1528
|
+
applyLifecycle(
|
|
1529
|
+
mergeEventTailItems(eventItems, seenEvents, liveRevisionBuffer.resolve(checkpoint.revision), include),
|
|
1530
|
+
);
|
|
1521
1531
|
if (gap !== undefined && args.strict === true)
|
|
1522
1532
|
throw new SdkSessionCliError(
|
|
1523
1533
|
"retention_gap",
|
|
@@ -1537,10 +1547,18 @@ async function runLiveTail(
|
|
|
1537
1547
|
);
|
|
1538
1548
|
throwResponseFailure(response);
|
|
1539
1549
|
const page = extractTranscriptPage(response);
|
|
1550
|
+
const rev = checkpoint?.revision;
|
|
1551
|
+
let nextSeq = transcriptItems.length;
|
|
1540
1552
|
mergeTailItems(
|
|
1541
1553
|
transcriptItems,
|
|
1542
1554
|
seenTranscript,
|
|
1543
|
-
page.items.map(item =>
|
|
1555
|
+
page.items.map(item => {
|
|
1556
|
+
const it = toTailItemV1(item, { kind: "transcript" });
|
|
1557
|
+
if (it.revision === undefined && rev !== undefined) it.revision = rev;
|
|
1558
|
+
if (it.generation === undefined) it.generation = checkpoint?.generation ?? 0;
|
|
1559
|
+
if (it.seq === undefined) it.seq = nextSeq++;
|
|
1560
|
+
return it;
|
|
1561
|
+
}),
|
|
1544
1562
|
include,
|
|
1545
1563
|
);
|
|
1546
1564
|
if (page.complete || page.cursor === undefined) break;
|
|
@@ -1586,9 +1604,24 @@ async function runLiveTail(
|
|
|
1586
1604
|
raw.seq,
|
|
1587
1605
|
);
|
|
1588
1606
|
}
|
|
1589
|
-
const replayItems = rawEvents.map(event =>
|
|
1607
|
+
const replayItems = rawEvents.map(event => {
|
|
1608
|
+
const it = toTailItemV1(event, { kind: "event" });
|
|
1609
|
+
if (it.revision === undefined && checkpoint?.revision !== undefined) it.revision = checkpoint.revision;
|
|
1610
|
+
return it;
|
|
1611
|
+
});
|
|
1590
1612
|
applyLifecycle(mergeEventTailItems(eventItems, seenEvents, replayItems, include));
|
|
1591
1613
|
if (malformed !== undefined) throw malformed;
|
|
1614
|
+
if (checkpoint !== undefined) {
|
|
1615
|
+
const checkpointKey = {
|
|
1616
|
+
revision: checkpoint.revision,
|
|
1617
|
+
generation: checkpoint.generation,
|
|
1618
|
+
seq: checkpoint.seq,
|
|
1619
|
+
};
|
|
1620
|
+
if (turnStateKey === undefined || compareTailSeqKeys(checkpointKey, turnStateKey) >= 0) {
|
|
1621
|
+
turnStateKey = checkpointKey;
|
|
1622
|
+
turnIdle = checkpoint.idle;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1592
1625
|
if (liveReason === undefined && args.untilIdle === true && turnIdle) liveReason = "idle";
|
|
1593
1626
|
if (liveReason === undefined) {
|
|
1594
1627
|
const completion = Promise.withResolvers<TailExitReason>();
|
|
@@ -291,8 +291,8 @@ function invoke(
|
|
|
291
291
|
return surface.setQueueMode("steering", input.mode);
|
|
292
292
|
case "queue.follow_up_mode.set":
|
|
293
293
|
return surface.setQueueMode("follow_up", input.mode);
|
|
294
|
-
case "queue.
|
|
295
|
-
return surface.setQueueMode("
|
|
294
|
+
case "queue.tool_interrupt_policy.set":
|
|
295
|
+
return surface.setQueueMode("tool_interrupt", input.mode);
|
|
296
296
|
case "compaction.run":
|
|
297
297
|
return surface.runCompaction();
|
|
298
298
|
case "compaction.auto.set":
|
package/src/sdk/host/events.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { SdkFrame } from "./types";
|
|
|
2
2
|
|
|
3
3
|
export interface EventFrame extends SdkFrame {
|
|
4
4
|
type: "event";
|
|
5
|
+
revision?: number;
|
|
5
6
|
generation: number;
|
|
6
7
|
seq: number;
|
|
7
8
|
}
|
|
@@ -31,11 +32,20 @@ export class SessionEventStream {
|
|
|
31
32
|
#frames: EventFrame[] = [];
|
|
32
33
|
readonly #ringSize: number;
|
|
33
34
|
readonly #resyncQueries: string[];
|
|
35
|
+
readonly #revisionProvider: (() => number | undefined) | undefined;
|
|
34
36
|
|
|
35
|
-
constructor(
|
|
37
|
+
constructor(
|
|
38
|
+
options: {
|
|
39
|
+
generation?: number;
|
|
40
|
+
ringSize?: number;
|
|
41
|
+
resyncQueryIds?: string[];
|
|
42
|
+
revisionProvider?: () => number | undefined;
|
|
43
|
+
} = {},
|
|
44
|
+
) {
|
|
36
45
|
this.#generation = options.generation ?? 0;
|
|
37
46
|
this.#ringSize = options.ringSize ?? 256;
|
|
38
47
|
this.#resyncQueries = options.resyncQueryIds ?? ["Q01", "Q02", "Q03"];
|
|
48
|
+
this.#revisionProvider = options.revisionProvider;
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
get generation(): number {
|
|
@@ -53,7 +63,21 @@ export class SessionEventStream {
|
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
emit(frame: SdkFrame): EventFrame {
|
|
56
|
-
const
|
|
66
|
+
const suppliedRevision = frame.revision;
|
|
67
|
+
const providedRevision = this.#revisionProvider?.();
|
|
68
|
+
const revision =
|
|
69
|
+
typeof suppliedRevision === "number" && Number.isSafeInteger(suppliedRevision) && suppliedRevision >= 0
|
|
70
|
+
? suppliedRevision
|
|
71
|
+
: typeof providedRevision === "number" && Number.isSafeInteger(providedRevision) && providedRevision >= 0
|
|
72
|
+
? providedRevision
|
|
73
|
+
: undefined;
|
|
74
|
+
const event: EventFrame = {
|
|
75
|
+
...frame,
|
|
76
|
+
type: "event",
|
|
77
|
+
...(revision === undefined ? {} : { revision }),
|
|
78
|
+
generation: this.#generation,
|
|
79
|
+
seq: ++this.#seq,
|
|
80
|
+
};
|
|
57
81
|
this.#frames.push(event);
|
|
58
82
|
if (this.#frames.length > this.#ringSize) this.#frames.shift();
|
|
59
83
|
return event;
|
package/src/sdk/host/host.ts
CHANGED
|
@@ -47,6 +47,8 @@ export type SessionActivationGate = (input: { sessionId: string; generation: num
|
|
|
47
47
|
export interface SessionSdkHostOptions extends HostEndpointAdapters {
|
|
48
48
|
control?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
|
|
49
49
|
query?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
|
|
50
|
+
/** Supplies the durable transcript revision stamped on ordinary emitted events. */
|
|
51
|
+
eventRevision?: () => number | undefined;
|
|
50
52
|
/** Test/lifecycle seam invoked synchronously before fire-and-forget dispatch. */
|
|
51
53
|
onFrameAdmitted?: (connectionId: string, frame: SdkFrame) => void;
|
|
52
54
|
/** Synchronously rejects external input while a runtime is being replaced. */
|
|
@@ -199,7 +201,7 @@ function has(frame: SdkFrame, field: string): boolean {
|
|
|
199
201
|
|
|
200
202
|
/** Adapter-based session host; bus wiring owns NotificationServer creation and transport framing. */
|
|
201
203
|
export class SessionSdkHost {
|
|
202
|
-
readonly events
|
|
204
|
+
readonly events: SessionEventStream;
|
|
203
205
|
readonly reverse: ReverseLeaseRuntime;
|
|
204
206
|
readonly #options: SessionSdkHostOptions;
|
|
205
207
|
#started = false;
|
|
@@ -214,6 +216,7 @@ export class SessionSdkHost {
|
|
|
214
216
|
|
|
215
217
|
constructor(options: SessionSdkHostOptions) {
|
|
216
218
|
this.#options = options;
|
|
219
|
+
this.events = new SessionEventStream({ revisionProvider: options.eventRevision });
|
|
217
220
|
this.reverse = new ReverseLeaseRuntime({
|
|
218
221
|
sendFrame: options.sendFrame,
|
|
219
222
|
installDefinitions: options.installProviderDefinitions,
|
|
@@ -354,6 +357,8 @@ export class SessionSdkHost {
|
|
|
354
357
|
if (this.events.generation !== generation) return "generation_changed";
|
|
355
358
|
if (this.#readyGeneration === generation) return "already";
|
|
356
359
|
}
|
|
360
|
+
if (!this.#started || this.#stopping) return "not_prepared";
|
|
361
|
+
if (this.events.generation !== generation) return "generation_changed";
|
|
357
362
|
this.#publishReadiness();
|
|
358
363
|
return "activated";
|
|
359
364
|
}
|
|
@@ -18,6 +18,7 @@ export interface CursorEnvelope {
|
|
|
18
18
|
issuedAt?: number;
|
|
19
19
|
/** TTL metadata minted by `CursorRegistry.grant`: wall-clock ms when the cursor expires. */
|
|
20
20
|
expiresAt?: number;
|
|
21
|
+
purpose?: "checkpoint";
|
|
21
22
|
position: unknown;
|
|
22
23
|
direction: string;
|
|
23
24
|
pageShape: unknown;
|
|
@@ -68,6 +69,7 @@ export interface CursorQueryContext {
|
|
|
68
69
|
resourceId?: string;
|
|
69
70
|
direction: string;
|
|
70
71
|
pageShape: unknown;
|
|
72
|
+
purpose?: "checkpoint";
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
export class CursorError extends Error {
|
|
@@ -170,26 +172,68 @@ export class CursorRegistry {
|
|
|
170
172
|
cursor: string,
|
|
171
173
|
connectionId: string,
|
|
172
174
|
expected: CursorQueryContext,
|
|
173
|
-
|
|
175
|
+
_resourceKind: string,
|
|
174
176
|
resourceId: string,
|
|
175
177
|
): Promise<{ cursor: string; envelope: CursorEnvelope }> {
|
|
176
178
|
const envelope = verifyCursor(cursor, this.sessionToken);
|
|
177
|
-
if (!envelope)
|
|
178
|
-
|
|
179
|
+
if (!envelope) {
|
|
180
|
+
this.release(cursor);
|
|
181
|
+
throw new CursorError("invalid_cursor", true);
|
|
182
|
+
}
|
|
183
|
+
const source = this.#active.get(cursor);
|
|
184
|
+
if (!source || envelope.expiresAt === undefined) {
|
|
185
|
+
this.release(cursor);
|
|
186
|
+
throw new CursorError("invalid_cursor", true);
|
|
187
|
+
}
|
|
188
|
+
const validatedAt = this.now();
|
|
189
|
+
if (source.expiresAt <= validatedAt || envelope.expiresAt <= validatedAt) {
|
|
190
|
+
this.release(cursor);
|
|
191
|
+
throw new CursorError("cursor_expired");
|
|
192
|
+
}
|
|
179
193
|
if (
|
|
180
194
|
envelope.sessionId !== expected.sessionId ||
|
|
181
195
|
(expected.resource !== undefined && envelope.resource !== expected.resource) ||
|
|
182
196
|
envelope.direction !== expected.direction ||
|
|
183
|
-
canonicalJson(envelope.pageShape) !== canonicalJson(expected.pageShape)
|
|
197
|
+
canonicalJson(envelope.pageShape) !== canonicalJson(expected.pageShape) ||
|
|
198
|
+
envelope.purpose !== expected.purpose ||
|
|
199
|
+
_resourceKind !== envelope.resource ||
|
|
200
|
+
source.resourceId !== resourceId
|
|
184
201
|
)
|
|
185
202
|
throw new CursorError("invalid_input", false, "cursor does not match query");
|
|
203
|
+
// Preserve the already-validated source while removing expired target
|
|
204
|
+
// grants before capacity accounting. The shared timestamp prevents the
|
|
205
|
+
// source from changing classification at a TTL boundary mid-exchange.
|
|
206
|
+
this.sweep(cursor, validatedAt);
|
|
207
|
+
const targetConnectionCount = [...this.#active.values()].filter(
|
|
208
|
+
item => item.connectionId === connectionId,
|
|
209
|
+
).length;
|
|
210
|
+
if (
|
|
211
|
+
this.#active.size > MAX_CURSORS_PER_SESSION ||
|
|
212
|
+
targetConnectionCount - (source.connectionId === connectionId ? 1 : 0) >= MAX_CURSORS_PER_CONNECTION
|
|
213
|
+
)
|
|
214
|
+
throw new CursorError("snapshot_capacity_exceeded", false);
|
|
186
215
|
const exchanged: CursorEnvelope = {
|
|
187
216
|
...envelope,
|
|
188
217
|
nonce: randomUUID(),
|
|
189
218
|
issuedAt: undefined,
|
|
190
219
|
expiresAt: undefined,
|
|
191
220
|
};
|
|
192
|
-
const
|
|
221
|
+
const issuedAt = this.now();
|
|
222
|
+
exchanged.issuedAt = issuedAt;
|
|
223
|
+
exchanged.expiresAt = issuedAt + CURSOR_TTL_MS;
|
|
224
|
+
const exchangedCursor = signCursor(exchanged, this.sessionToken);
|
|
225
|
+
try {
|
|
226
|
+
this.revisions.transferPin(cursor, exchangedCursor);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error instanceof RevisionStoreError) throw new CursorError(error.code, false, error.message);
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
this.#active.delete(cursor);
|
|
232
|
+
this.#active.set(exchangedCursor, {
|
|
233
|
+
connectionId,
|
|
234
|
+
resourceId,
|
|
235
|
+
expiresAt: exchanged.expiresAt,
|
|
236
|
+
});
|
|
193
237
|
return { cursor: exchangedCursor, envelope: exchanged };
|
|
194
238
|
}
|
|
195
239
|
|
|
@@ -227,8 +271,9 @@ export class CursorRegistry {
|
|
|
227
271
|
close(): void {
|
|
228
272
|
for (const cursor of this.#active.keys()) this.release(cursor);
|
|
229
273
|
}
|
|
230
|
-
sweep(): void {
|
|
231
|
-
for (const [cursor, active] of this.#active)
|
|
274
|
+
sweep(exceptCursor?: string, at = this.now()): void {
|
|
275
|
+
for (const [cursor, active] of this.#active)
|
|
276
|
+
if (cursor !== exceptCursor && active.expiresAt <= at) this.release(cursor);
|
|
232
277
|
this.revisions.sweep();
|
|
233
278
|
}
|
|
234
279
|
get size(): number {
|
|
@@ -108,6 +108,7 @@ export interface SdkCheckpointRecord {
|
|
|
108
108
|
revision: number;
|
|
109
109
|
generation: number;
|
|
110
110
|
seq: number;
|
|
111
|
+
idle: boolean;
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
function isNonNegativeSafeInteger(value: unknown): value is number {
|
|
@@ -119,10 +120,11 @@ export function isCheckpointRecord(value: unknown): value is SdkCheckpointRecord
|
|
|
119
120
|
const record = value as Record<string, unknown>;
|
|
120
121
|
const keys = Object.keys(record);
|
|
121
122
|
return (
|
|
122
|
-
keys.length ===
|
|
123
|
+
keys.length === 4 &&
|
|
123
124
|
isNonNegativeSafeInteger(record.revision) &&
|
|
124
125
|
isNonNegativeSafeInteger(record.generation) &&
|
|
125
|
-
isNonNegativeSafeInteger(record.seq)
|
|
126
|
+
isNonNegativeSafeInteger(record.seq) &&
|
|
127
|
+
typeof record.idle === "boolean"
|
|
126
128
|
);
|
|
127
129
|
}
|
|
128
130
|
const sources: Record<string, { resource: string; method: keyof SessionSurface; mvcc: boolean }> = {
|
|
@@ -296,9 +298,10 @@ export class QueryHandlers {
|
|
|
296
298
|
let snapshot: unknown;
|
|
297
299
|
// The cursor may arrive either as the top-level continuation cursor or,
|
|
298
300
|
// on Q01 only (validated in `dispatch`), as the `checkpointToken` resume
|
|
299
|
-
// seed. Both are
|
|
300
|
-
//
|
|
301
|
-
const
|
|
301
|
+
// seed. Both are consumed through the same CursorRegistry authority, but
|
|
302
|
+
// only the named input token is required to carry checkpoint purpose.
|
|
303
|
+
const checkpointToken = queryId === "Q01" ? request.input?.checkpointToken : undefined;
|
|
304
|
+
const rawCursorToken = request.cursor ?? checkpointToken;
|
|
302
305
|
if (rawCursorToken !== undefined && typeof rawCursorToken !== "string")
|
|
303
306
|
return this.#error(request, "invalid_request", false, "checkpointToken must be a non-empty string.");
|
|
304
307
|
const cursorToken = rawCursorToken;
|
|
@@ -309,6 +312,8 @@ export class QueryHandlers {
|
|
|
309
312
|
direction: "forward",
|
|
310
313
|
pageShape: { targetBytes: TARGET_PAGE_BYTES },
|
|
311
314
|
});
|
|
315
|
+
if (checkpointToken !== undefined && cursor.purpose !== "checkpoint")
|
|
316
|
+
throw new CursorError("invalid_input", false, "checkpointToken does not carry checkpoint purpose");
|
|
312
317
|
selector = assertCursorSelector(cursorSelector(cursor.position), selector);
|
|
313
318
|
resourceId = selector.resourceId ?? "default";
|
|
314
319
|
revision = cursor.revision;
|
|
@@ -467,10 +472,20 @@ export class QueryHandlers {
|
|
|
467
472
|
resource: "transcript",
|
|
468
473
|
direction: "forward",
|
|
469
474
|
pageShape: { targetBytes: TARGET_PAGE_BYTES },
|
|
475
|
+
purpose: "checkpoint",
|
|
470
476
|
},
|
|
471
477
|
"transcript",
|
|
472
478
|
"default",
|
|
473
479
|
);
|
|
480
|
+
if (!isCheckpointRecord(exchanged.envelope.highWatermark)) {
|
|
481
|
+
this.cursors.release(exchanged.cursor);
|
|
482
|
+
return this.#error(
|
|
483
|
+
request,
|
|
484
|
+
"invalid_input",
|
|
485
|
+
false,
|
|
486
|
+
"checkpointToken is not an authoritative checkpoint.",
|
|
487
|
+
);
|
|
488
|
+
}
|
|
474
489
|
return {
|
|
475
490
|
id: request.id,
|
|
476
491
|
ok: true,
|
|
@@ -494,13 +509,18 @@ export class QueryHandlers {
|
|
|
494
509
|
// Atomic synchronous capture (C9): entries and event-ring watermark come
|
|
495
510
|
// from the same host-owned call, so the pinned snapshot revision and the
|
|
496
511
|
// subscribe position can never straddle a concurrent append.
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
512
|
+
if (typeof this.surface.getCheckpointSnapshot !== "function")
|
|
513
|
+
return this.#error(request, "unavailable", false, "Atomic session checkpoint capture is unavailable.");
|
|
514
|
+
const captured = this.surface.getCheckpointSnapshot();
|
|
515
|
+
const entries = captured.entries;
|
|
516
|
+
const head = captured.watermark;
|
|
517
|
+
if (!isCheckpointRecord(head))
|
|
518
|
+
return this.#error(
|
|
519
|
+
request,
|
|
520
|
+
"unavailable",
|
|
521
|
+
false,
|
|
522
|
+
"Atomic session checkpoint capture returned an invalid watermark.",
|
|
523
|
+
);
|
|
504
524
|
// Mint the snapshot from the exact entries captured with the watermark;
|
|
505
525
|
// entries appended after this point are excluded from replay by
|
|
506
526
|
// construction (append-during-checkpoint), and the returned token is the
|
|
@@ -517,6 +537,7 @@ export class QueryHandlers {
|
|
|
517
537
|
highWatermark: head,
|
|
518
538
|
nonce,
|
|
519
539
|
position: { offset: 0, selector: { queryId: "Q01" } },
|
|
540
|
+
purpose: "checkpoint",
|
|
520
541
|
direction: "forward",
|
|
521
542
|
pageShape: { targetBytes: TARGET_PAGE_BYTES },
|
|
522
543
|
};
|
|
@@ -524,6 +524,18 @@ export class RevisionStore {
|
|
|
524
524
|
await this.#enforceMemory();
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
+
transferPin(sourceCursorId: string, replacementCursorId: string): void {
|
|
528
|
+
if (sourceCursorId === replacementCursorId) return;
|
|
529
|
+
if (this.#pinIndex.has(replacementCursorId))
|
|
530
|
+
throw new RevisionStoreError("snapshot_capacity_exceeded", "replacement cursor is already pinned");
|
|
531
|
+
const revision = this.#pinIndex.get(sourceCursorId);
|
|
532
|
+
if (!revision) throw new RevisionStoreError("snapshot_capacity_exceeded", "snapshot pin is unavailable");
|
|
533
|
+
revision.pins.delete(sourceCursorId);
|
|
534
|
+
revision.pins.add(replacementCursorId);
|
|
535
|
+
this.#pinIndex.delete(sourceCursorId);
|
|
536
|
+
this.#pinIndex.set(replacementCursorId, revision);
|
|
537
|
+
}
|
|
538
|
+
|
|
527
539
|
unpin(cursorId: string): void {
|
|
528
540
|
const revision = this.#pinIndex.get(cursorId);
|
|
529
541
|
if (!revision) return;
|