@trycadence/cli 0.1.10-dev.0 → 0.1.14-dev.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 +32 -0
- package/dist/cadence +1573 -164
- package/package.json +1 -1
package/dist/cadence
CHANGED
|
@@ -1513,9 +1513,39 @@ function createCadenceClient(options = {}) {
|
|
|
1513
1513
|
// src/index.ts
|
|
1514
1514
|
import { spawn, spawnSync } from "child_process";
|
|
1515
1515
|
import { createHash, randomUUID } from "crypto";
|
|
1516
|
+
import { existsSync, readdirSync, readFileSync as readFileSyncNode, statSync } from "fs";
|
|
1516
1517
|
import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
|
|
1517
1518
|
import { basename, dirname, isAbsolute, join, parse } from "path";
|
|
1518
1519
|
import { createInterface } from "readline/promises";
|
|
1520
|
+
// package.json
|
|
1521
|
+
var package_default = {
|
|
1522
|
+
name: "@trycadence/cli",
|
|
1523
|
+
version: "0.1.14-dev.0",
|
|
1524
|
+
private: false,
|
|
1525
|
+
type: "module",
|
|
1526
|
+
bin: {
|
|
1527
|
+
cadence: "dist/cadence"
|
|
1528
|
+
},
|
|
1529
|
+
files: [
|
|
1530
|
+
"dist",
|
|
1531
|
+
"README.md"
|
|
1532
|
+
],
|
|
1533
|
+
scripts: {
|
|
1534
|
+
build: "bun build src/index.ts --target=bun --outfile=dist/cadence",
|
|
1535
|
+
dev: "bun run src/index.ts",
|
|
1536
|
+
prepack: "bun run build",
|
|
1537
|
+
test: "bun test",
|
|
1538
|
+
typecheck: "tsc --noEmit -p tsconfig.json"
|
|
1539
|
+
},
|
|
1540
|
+
engines: {
|
|
1541
|
+
bun: ">=1.3.0"
|
|
1542
|
+
},
|
|
1543
|
+
publishConfig: {
|
|
1544
|
+
access: "public"
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
|
|
1548
|
+
// src/index.ts
|
|
1519
1549
|
var ticketPriorities = ["low", "normal", "high", "urgent"];
|
|
1520
1550
|
var ticketStatuses = ["backlog", "ready", "in_progress", "blocked", "review", "done", "abandoned"];
|
|
1521
1551
|
var sessionFileChangeKinds = ["added", "modified", "deleted", "renamed", "unknown"];
|
|
@@ -1524,19 +1554,19 @@ var workLogParentSelectors = ["last", "ticket-last", "session-last", "last-decis
|
|
|
1524
1554
|
var changesetPrNoteSources = ["agent", "human", "system"];
|
|
1525
1555
|
var hookScopes = ["repo", "global", "both"];
|
|
1526
1556
|
var agentEventSources = ["codex", "claude-code", "opencode", "openrouter", "unknown"];
|
|
1527
|
-
var defaultLeaseTtlSeconds =
|
|
1557
|
+
var defaultLeaseTtlSeconds = 15 * 60;
|
|
1528
1558
|
var defaultCliApiBaseUrl = "https://cadenceapi.deploy.lvl8studios.com";
|
|
1529
1559
|
var defaultCliWebBaseUrl = "https://cadence.deploy.lvl8studios.com";
|
|
1530
|
-
var
|
|
1531
|
-
var defaultCheckpointThresholdMax = 5;
|
|
1560
|
+
var defaultCheckpointThreshold = 3;
|
|
1532
1561
|
var defaultCheckpointCooldownSeconds = 10 * 60;
|
|
1533
1562
|
var defaultCheckpointWorkerTimeoutMs = 10 * 60 * 1000;
|
|
1534
|
-
var defaultHookCommand = "cadence agent-run ingest-stop --source codex --event stop
|
|
1563
|
+
var defaultHookCommand = `/bin/sh -lc 'root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0; cd "$root" || exit 0; [ -f .cadence/config.json ] || [ -f .cadence/config.local.json ] || exit 0; exec cadence agent-run ingest-stop --source codex --event stop >/dev/null'`;
|
|
1535
1564
|
var agentLoopSuppressEnv = "CADENCE_AGENT_EVENT_SUPPRESS";
|
|
1536
1565
|
var credentialRefreshSkewMs = 60 * 1000;
|
|
1537
1566
|
var credentialRefreshLockTimeoutMs = 10 * 1000;
|
|
1538
1567
|
var credentialRefreshLockStaleMs = 60 * 1000;
|
|
1539
1568
|
var credentialRefreshLockPollMs = 100;
|
|
1569
|
+
var cliVersion = package_default.version;
|
|
1540
1570
|
|
|
1541
1571
|
class CliError extends Error {
|
|
1542
1572
|
code;
|
|
@@ -1569,6 +1599,7 @@ var knownCommandPaths = [
|
|
|
1569
1599
|
["changesets", "notes", "put"],
|
|
1570
1600
|
["changesets", "notes", "apply"],
|
|
1571
1601
|
["agent-run", "ingest-stop"],
|
|
1602
|
+
["agent-run", "checkpoint"],
|
|
1572
1603
|
["agent-run", "closeout"],
|
|
1573
1604
|
["agent-run", "sweep"],
|
|
1574
1605
|
["agent-run", "doctor"],
|
|
@@ -1594,6 +1625,7 @@ var knownCommandPaths = [
|
|
|
1594
1625
|
["projects", "list"],
|
|
1595
1626
|
["init"],
|
|
1596
1627
|
["status"],
|
|
1628
|
+
["version"],
|
|
1597
1629
|
["help"]
|
|
1598
1630
|
];
|
|
1599
1631
|
function isCommandMatch(words, commandPath) {
|
|
@@ -1623,6 +1655,7 @@ function parseCliArgs(argv) {
|
|
|
1623
1655
|
const options = {};
|
|
1624
1656
|
let json = false;
|
|
1625
1657
|
let help = false;
|
|
1658
|
+
let version = false;
|
|
1626
1659
|
let server;
|
|
1627
1660
|
let project;
|
|
1628
1661
|
for (let index = 0;index < argv.length; index += 1) {
|
|
@@ -1638,6 +1671,10 @@ function parseCliArgs(argv) {
|
|
|
1638
1671
|
help = true;
|
|
1639
1672
|
continue;
|
|
1640
1673
|
}
|
|
1674
|
+
if (arg === "--version" || arg === "-v") {
|
|
1675
|
+
version = true;
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1641
1678
|
if (arg === "--server") {
|
|
1642
1679
|
server = readFlagValue(argv, index, arg);
|
|
1643
1680
|
index += 1;
|
|
@@ -1668,6 +1705,7 @@ ${value}` : value;
|
|
|
1668
1705
|
flags: {
|
|
1669
1706
|
json,
|
|
1670
1707
|
help,
|
|
1708
|
+
version,
|
|
1671
1709
|
...server ? { server } : {},
|
|
1672
1710
|
...project ? { project } : {}
|
|
1673
1711
|
},
|
|
@@ -2207,6 +2245,8 @@ function helpText() {
|
|
|
2207
2245
|
"Cadence CLI",
|
|
2208
2246
|
"",
|
|
2209
2247
|
"Usage:",
|
|
2248
|
+
" cadence --version",
|
|
2249
|
+
" cadence version [--json]",
|
|
2210
2250
|
" cadence init [--project <project-id|org/project>] [--json]",
|
|
2211
2251
|
" cadence auth login [--json]",
|
|
2212
2252
|
" cadence auth status [--json]",
|
|
@@ -2221,7 +2261,7 @@ function helpText() {
|
|
|
2221
2261
|
" cadence tickets attach <ticket-id> --from-intake <intake-id> --if-version <version> [--project <project-id>] [--json]",
|
|
2222
2262
|
" cadence tickets create --title <text> [--from-intake <intake-id>] [--project <project-id>] [--json]",
|
|
2223
2263
|
" cadence tickets update <ticket-id> --if-version <version> [--title <text>] [--description <text>] [--priority <priority>] [--status <status>] [--project <project-id>] [--json]",
|
|
2224
|
-
" cadence tickets claim <ticket-id> --session <session-id> [--actor <actor-id>] [--project <project-id>] [--json]",
|
|
2264
|
+
" cadence tickets claim <ticket-id> --session <session-id> [--actor <actor-id>] [--replace-own-active true|false] [--project <project-id>] [--json]",
|
|
2225
2265
|
" cadence tickets release <ticket-id> --lease <lease-id> [--project <project-id>] [--json]",
|
|
2226
2266
|
" cadence tickets log <ticket-id> --kind <intent|decision|rationale|action|verification|blocker|correction|note> --body <text> [--summary <text>] [--under <entry-id|ticket-last|session-last|last-decision|last-correction|last-action>] [--session <session-id>] [--changeset <changeset-id>] [--project <project-id>] [--json]",
|
|
2227
2267
|
" cadence tickets complete <ticket-id> --if-version <version> [--summary <summary>] [--project <project-id>] [--json]",
|
|
@@ -2239,16 +2279,17 @@ function helpText() {
|
|
|
2239
2279
|
" cadence changesets notes put [--changeset <id>|--branch current|<branch>] --title <text> --body-file <path> [--head-sha <sha>] [--base-sha <sha>] [--pr-url <url>] [--pr-number <n>] [--project <project-id>] [--json]",
|
|
2240
2280
|
" cadence changesets notes apply [--changeset <id>|--branch current|<branch>] --provider github --pr-number <n> --pr-url <url> [--project <project-id>] [--json]",
|
|
2241
2281
|
" cadence agent-run ingest-stop --source <codex|claude-code|opencode|openrouter> [--event <event>] [--threshold <n>] [--dry-run true|false] [--project <project-id>] [--json]",
|
|
2242
|
-
" cadence agent-run
|
|
2282
|
+
" cadence agent-run checkpoint --agent-session-key <key> [--reason <threshold|idle|manual>] [--event-file <path>] [--ticket <ticket-id>] [--session <session-id>] [--changeset <changeset-id>] [--checkpoint-provider codex] [--checkpoint-model <model>] [--log-kind <kind>] [--update-summary true|false] [--json]",
|
|
2243
2283
|
" cadence agent-run sweep [--idle-after-seconds <n>] [--dry-run true|false] [--json]",
|
|
2244
2284
|
" cadence agent-run doctor [--json]",
|
|
2245
|
-
" cadence hooks install --provider codex --scope
|
|
2246
|
-
" cadence hooks doctor --provider codex --scope
|
|
2285
|
+
" cadence hooks install --provider codex [--scope global] [--command <command>] [--json]",
|
|
2286
|
+
" cadence hooks doctor --provider codex [--scope global] [--json]",
|
|
2247
2287
|
" cadence events list [--project <project-id>] [--ticket <ticket-id>] [--changeset <changeset-id>] [--session <session-id>] [--json]",
|
|
2248
2288
|
"",
|
|
2249
2289
|
"Global flags:",
|
|
2250
2290
|
" --project <id> Cadence project ID or org/project slug",
|
|
2251
2291
|
" --server <url> Cadence API server override",
|
|
2292
|
+
" --version, -v Print the Cadence CLI version",
|
|
2252
2293
|
" --json Print stable JSON envelope",
|
|
2253
2294
|
"",
|
|
2254
2295
|
"Work log parent selectors:",
|
|
@@ -2541,6 +2582,14 @@ function truncateText(value, maxLength) {
|
|
|
2541
2582
|
return `${value.slice(0, maxLength - 15)}
|
|
2542
2583
|
[truncated]`;
|
|
2543
2584
|
}
|
|
2585
|
+
function estimateTokenCount(value) {
|
|
2586
|
+
const text = value.trim();
|
|
2587
|
+
if (!text) {
|
|
2588
|
+
return 0;
|
|
2589
|
+
}
|
|
2590
|
+
const pieces = text.match(/[A-Za-z0-9_]+|[^\sA-Za-z0-9_]/g) ?? [];
|
|
2591
|
+
return Math.max(pieces.length, Math.ceil(text.length / 4));
|
|
2592
|
+
}
|
|
2544
2593
|
function stableHash(value) {
|
|
2545
2594
|
return createHash("sha256").update(value).digest("hex");
|
|
2546
2595
|
}
|
|
@@ -2579,13 +2628,392 @@ function firstString(record, paths) {
|
|
|
2579
2628
|
}
|
|
2580
2629
|
return;
|
|
2581
2630
|
}
|
|
2631
|
+
function readRecentAgentTurns(input) {
|
|
2632
|
+
const rawTurns = input.recent_turns ?? input.recentTurns ?? input.turns;
|
|
2633
|
+
if (!Array.isArray(rawTurns)) {
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
const turns = rawTurns.map((turn) => {
|
|
2637
|
+
if (!turn || typeof turn !== "object" || Array.isArray(turn)) {
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
const record = turn;
|
|
2641
|
+
const user = firstString(record, [["user"], ["prompt"], ["userPrompt"], ["request"]]);
|
|
2642
|
+
const assistant = firstString(record, [["assistant"], ["response"], ["assistantResponse"], ["reply"]]);
|
|
2643
|
+
const occurredAt = firstString(record, [["occurredAt"], ["occurred_at"], ["timestamp"], ["createdAt"]]);
|
|
2644
|
+
if (!user && !assistant) {
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
return {
|
|
2648
|
+
...user ? { user: truncateText(user, 6000) } : {},
|
|
2649
|
+
...assistant ? { assistant: truncateText(assistant, 6000) } : {},
|
|
2650
|
+
...occurredAt ? { occurredAt } : {}
|
|
2651
|
+
};
|
|
2652
|
+
}).filter((turn) => Boolean(turn));
|
|
2653
|
+
return turns.length ? turns.slice(-3) : undefined;
|
|
2654
|
+
}
|
|
2655
|
+
function mergeRecentAgentTurns(existing, next) {
|
|
2656
|
+
if (!next?.length) {
|
|
2657
|
+
return existing?.length ? existing.slice(-3) : undefined;
|
|
2658
|
+
}
|
|
2659
|
+
return [...existing ?? [], ...next].slice(-3);
|
|
2660
|
+
}
|
|
2661
|
+
function stringFromCodexContent(value) {
|
|
2662
|
+
if (typeof value === "string") {
|
|
2663
|
+
return value.trim() || undefined;
|
|
2664
|
+
}
|
|
2665
|
+
if (Array.isArray(value)) {
|
|
2666
|
+
const text = value.map((item) => {
|
|
2667
|
+
if (typeof item === "string") {
|
|
2668
|
+
return item;
|
|
2669
|
+
}
|
|
2670
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
2671
|
+
const record = item;
|
|
2672
|
+
return typeof record.text === "string" ? record.text : typeof record.content === "string" ? record.content : "";
|
|
2673
|
+
}
|
|
2674
|
+
return "";
|
|
2675
|
+
}).join(`
|
|
2676
|
+
`).trim();
|
|
2677
|
+
return text || undefined;
|
|
2678
|
+
}
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
function findCodexSessionFile(directory, agentSessionId, depth = 0) {
|
|
2682
|
+
if (depth > 6 || !existsSync(directory)) {
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
let entries;
|
|
2686
|
+
try {
|
|
2687
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
2688
|
+
} catch {
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
for (const entry of entries) {
|
|
2692
|
+
const entryPath = join(directory, entry.name);
|
|
2693
|
+
if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.includes(agentSessionId)) {
|
|
2694
|
+
return entryPath;
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
for (const entry of entries) {
|
|
2698
|
+
if (!entry.isDirectory()) {
|
|
2699
|
+
continue;
|
|
2700
|
+
}
|
|
2701
|
+
const found = findCodexSessionFile(join(directory, entry.name), agentSessionId, depth + 1);
|
|
2702
|
+
if (found) {
|
|
2703
|
+
return found;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
function codexSessionsDirectory(options) {
|
|
2709
|
+
const home = options.env?.HOME ?? process.env.HOME;
|
|
2710
|
+
const codexHome = options.env?.CODEX_HOME ?? process.env.CODEX_HOME ?? (home ? join(home, ".codex") : undefined);
|
|
2711
|
+
return codexHome ? join(codexHome, "sessions") : undefined;
|
|
2712
|
+
}
|
|
2713
|
+
function listCodexSessionFiles(directory, depth = 0) {
|
|
2714
|
+
if (depth > 6 || !existsSync(directory)) {
|
|
2715
|
+
return [];
|
|
2716
|
+
}
|
|
2717
|
+
let entries;
|
|
2718
|
+
try {
|
|
2719
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
2720
|
+
} catch {
|
|
2721
|
+
return [];
|
|
2722
|
+
}
|
|
2723
|
+
return entries.flatMap((entry) => {
|
|
2724
|
+
const entryPath = join(directory, entry.name);
|
|
2725
|
+
if (entry.isDirectory()) {
|
|
2726
|
+
return listCodexSessionFiles(entryPath, depth + 1);
|
|
2727
|
+
}
|
|
2728
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
|
|
2729
|
+
return [];
|
|
2730
|
+
}
|
|
2731
|
+
try {
|
|
2732
|
+
return [{ path: entryPath, mtimeMs: statSync(entryPath).mtimeMs }];
|
|
2733
|
+
} catch {
|
|
2734
|
+
return [];
|
|
2735
|
+
}
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
function readCodexSessionMeta(filePath) {
|
|
2739
|
+
let lines;
|
|
2740
|
+
try {
|
|
2741
|
+
lines = readFileSyncNode(filePath, "utf8").split(`
|
|
2742
|
+
`).filter((line) => line.trim());
|
|
2743
|
+
} catch {
|
|
2744
|
+
return {};
|
|
2745
|
+
}
|
|
2746
|
+
for (const line of lines) {
|
|
2747
|
+
let parsed;
|
|
2748
|
+
try {
|
|
2749
|
+
parsed = JSON.parse(line);
|
|
2750
|
+
} catch {
|
|
2751
|
+
continue;
|
|
2752
|
+
}
|
|
2753
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2756
|
+
const event = parsed;
|
|
2757
|
+
const payload = event.payload && typeof event.payload === "object" && !Array.isArray(event.payload) ? event.payload : {};
|
|
2758
|
+
if (event.type !== "session_meta") {
|
|
2759
|
+
continue;
|
|
2760
|
+
}
|
|
2761
|
+
return {
|
|
2762
|
+
...typeof payload.id === "string" ? { id: payload.id } : {},
|
|
2763
|
+
...typeof payload.cwd === "string" ? { cwd: payload.cwd } : {},
|
|
2764
|
+
...typeof event.timestamp === "string" ? { timestamp: event.timestamp } : {}
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
return {};
|
|
2768
|
+
}
|
|
2769
|
+
function findCodexSessionFileCreatedAfter(options, startedAtMs, cwd) {
|
|
2770
|
+
const sessionsDirectory = codexSessionsDirectory(options);
|
|
2771
|
+
if (!sessionsDirectory) {
|
|
2772
|
+
return;
|
|
2773
|
+
}
|
|
2774
|
+
const candidates = listCodexSessionFiles(sessionsDirectory).filter((candidate) => candidate.mtimeMs >= startedAtMs - 1000).map((candidate) => ({
|
|
2775
|
+
...candidate,
|
|
2776
|
+
meta: readCodexSessionMeta(candidate.path)
|
|
2777
|
+
})).filter((candidate) => !candidate.meta.cwd || candidate.meta.cwd === cwd).sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
2778
|
+
return candidates[0]?.path;
|
|
2779
|
+
}
|
|
2780
|
+
function codexTranscriptTextFromPayload(payload) {
|
|
2781
|
+
return stringFromCodexContent(payload.message) ?? stringFromCodexContent(payload.text) ?? stringFromCodexContent(payload.content) ?? stringFromCodexContent(payload.output) ?? undefined;
|
|
2782
|
+
}
|
|
2783
|
+
function summarizeCodexTranscriptEvent(raw, index) {
|
|
2784
|
+
const payload = raw.payload && typeof raw.payload === "object" && !Array.isArray(raw.payload) ? raw.payload : {};
|
|
2785
|
+
const type = typeof raw.type === "string" ? raw.type : "unknown";
|
|
2786
|
+
const payloadType = typeof payload.type === "string" ? payload.type : undefined;
|
|
2787
|
+
const text = codexTranscriptTextFromPayload(payload);
|
|
2788
|
+
const callId = typeof payload.call_id === "string" ? payload.call_id : typeof payload.callId === "string" ? payload.callId : undefined;
|
|
2789
|
+
const toolArguments = typeof payload.arguments === "string" ? payload.arguments : payload.arguments && typeof payload.arguments === "object" ? JSON.stringify(payload.arguments) : undefined;
|
|
2790
|
+
const toolName = typeof payload.tool_name === "string" ? payload.tool_name : typeof payload.toolName === "string" ? payload.toolName : typeof payload.name === "string" ? payload.name : undefined;
|
|
2791
|
+
return {
|
|
2792
|
+
index,
|
|
2793
|
+
type,
|
|
2794
|
+
...payloadType ? { payloadType } : {},
|
|
2795
|
+
...typeof raw.timestamp === "string" ? { timestamp: raw.timestamp } : {},
|
|
2796
|
+
...text ? { text: truncateText(text, 8000) } : {},
|
|
2797
|
+
...callId ? { callId } : {},
|
|
2798
|
+
...toolName ? { toolName } : {},
|
|
2799
|
+
...toolArguments ? { toolArguments: truncateText(toolArguments, 8000) } : {},
|
|
2800
|
+
raw
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
function numberFromRecord(record, key) {
|
|
2804
|
+
const value = record[key];
|
|
2805
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
2806
|
+
}
|
|
2807
|
+
function codexTokenUsageRecord(value) {
|
|
2808
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
const record = value;
|
|
2812
|
+
return {
|
|
2813
|
+
...numberFromRecord(record, "input_tokens") !== undefined ? { inputTokens: numberFromRecord(record, "input_tokens") } : {},
|
|
2814
|
+
...numberFromRecord(record, "cached_input_tokens") !== undefined ? { cachedInputTokens: numberFromRecord(record, "cached_input_tokens") } : {},
|
|
2815
|
+
...numberFromRecord(record, "output_tokens") !== undefined ? { outputTokens: numberFromRecord(record, "output_tokens") } : {},
|
|
2816
|
+
...numberFromRecord(record, "reasoning_output_tokens") !== undefined ? { reasoningOutputTokens: numberFromRecord(record, "reasoning_output_tokens") } : {},
|
|
2817
|
+
...numberFromRecord(record, "total_tokens") !== undefined ? { totalTokens: numberFromRecord(record, "total_tokens") } : {}
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
function extractCodexTokenUsage(events) {
|
|
2821
|
+
const tokenEvents = events.filter((event) => event.payloadType === "token_count");
|
|
2822
|
+
if (!tokenEvents.length) {
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
let total;
|
|
2826
|
+
let last;
|
|
2827
|
+
let modelContextWindow;
|
|
2828
|
+
let updatedAt;
|
|
2829
|
+
for (const event of tokenEvents) {
|
|
2830
|
+
const payload = event.raw.payload && typeof event.raw.payload === "object" && !Array.isArray(event.raw.payload) ? event.raw.payload : {};
|
|
2831
|
+
const info = payload.info && typeof payload.info === "object" && !Array.isArray(payload.info) ? payload.info : {};
|
|
2832
|
+
const parsedTotal = codexTokenUsageRecord(info.total_token_usage);
|
|
2833
|
+
const parsedLast = codexTokenUsageRecord(info.last_token_usage);
|
|
2834
|
+
const parsedContextWindow = numberFromRecord(info, "model_context_window");
|
|
2835
|
+
if (parsedTotal && Object.keys(parsedTotal).length) {
|
|
2836
|
+
total = parsedTotal;
|
|
2837
|
+
}
|
|
2838
|
+
if (parsedLast && Object.keys(parsedLast).length) {
|
|
2839
|
+
last = parsedLast;
|
|
2840
|
+
}
|
|
2841
|
+
if (parsedContextWindow !== undefined) {
|
|
2842
|
+
modelContextWindow = parsedContextWindow;
|
|
2843
|
+
}
|
|
2844
|
+
updatedAt = event.timestamp;
|
|
2845
|
+
}
|
|
2846
|
+
return {
|
|
2847
|
+
source: "codex_session_transcript",
|
|
2848
|
+
eventCount: tokenEvents.length,
|
|
2849
|
+
...total ? { total } : {},
|
|
2850
|
+
...last ? { last } : {},
|
|
2851
|
+
...modelContextWindow !== undefined ? { modelContextWindow } : {},
|
|
2852
|
+
...updatedAt ? { updatedAt } : {}
|
|
2853
|
+
};
|
|
2854
|
+
}
|
|
2855
|
+
function tokenUsageTotal(tokenUsage) {
|
|
2856
|
+
return tokenUsage?.total;
|
|
2857
|
+
}
|
|
2858
|
+
function roundedRatio(numerator, denominator) {
|
|
2859
|
+
if (denominator <= 0) {
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
return Math.round(numerator / denominator * 1000) / 1000;
|
|
2863
|
+
}
|
|
2864
|
+
function readCodexSessionTranscript(filePath) {
|
|
2865
|
+
if (!filePath) {
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
let lines;
|
|
2869
|
+
try {
|
|
2870
|
+
lines = readFileSyncNode(filePath, "utf8").split(`
|
|
2871
|
+
`).filter((line) => line.trim());
|
|
2872
|
+
} catch {
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
const events = lines.flatMap((line, index) => {
|
|
2876
|
+
try {
|
|
2877
|
+
const parsed = JSON.parse(line);
|
|
2878
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2879
|
+
return [];
|
|
2880
|
+
}
|
|
2881
|
+
return [summarizeCodexTranscriptEvent(parsed, index)];
|
|
2882
|
+
} catch {
|
|
2883
|
+
return [
|
|
2884
|
+
{
|
|
2885
|
+
index,
|
|
2886
|
+
type: "unparsed",
|
|
2887
|
+
text: truncateText(line, 8000),
|
|
2888
|
+
raw: { line }
|
|
2889
|
+
}
|
|
2890
|
+
];
|
|
2891
|
+
}
|
|
2892
|
+
});
|
|
2893
|
+
return {
|
|
2894
|
+
path: filePath,
|
|
2895
|
+
fileName: basename(filePath),
|
|
2896
|
+
lineCount: lines.length,
|
|
2897
|
+
...extractCodexTokenUsage(events) ? { tokenUsage: extractCodexTokenUsage(events) } : {},
|
|
2898
|
+
events
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
function readRecentTurnsFromCodexSessionFile(filePath) {
|
|
2902
|
+
let lines;
|
|
2903
|
+
try {
|
|
2904
|
+
lines = readFileSyncNode(filePath, "utf8").split(`
|
|
2905
|
+
`).filter((line) => line.trim());
|
|
2906
|
+
} catch {
|
|
2907
|
+
return;
|
|
2908
|
+
}
|
|
2909
|
+
const messages = [];
|
|
2910
|
+
const pushMessage = (message) => {
|
|
2911
|
+
const previous = messages[messages.length - 1];
|
|
2912
|
+
if (previous?.role === message.role && previous.text === message.text) {
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
messages.push(message);
|
|
2916
|
+
};
|
|
2917
|
+
for (const line of lines) {
|
|
2918
|
+
let parsed;
|
|
2919
|
+
try {
|
|
2920
|
+
parsed = JSON.parse(line);
|
|
2921
|
+
} catch {
|
|
2922
|
+
continue;
|
|
2923
|
+
}
|
|
2924
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2925
|
+
continue;
|
|
2926
|
+
}
|
|
2927
|
+
const event = parsed;
|
|
2928
|
+
const payload = event.payload && typeof event.payload === "object" && !Array.isArray(event.payload) ? event.payload : undefined;
|
|
2929
|
+
if (event.type === "event_msg" && payload?.type === "user_message") {
|
|
2930
|
+
const text = stringFromCodexContent(payload.message);
|
|
2931
|
+
if (text) {
|
|
2932
|
+
pushMessage({
|
|
2933
|
+
role: "user",
|
|
2934
|
+
text,
|
|
2935
|
+
...typeof event.timestamp === "string" ? { occurredAt: event.timestamp } : {}
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
if (event.type === "event_msg" && payload?.type === "agent_message") {
|
|
2940
|
+
const text = stringFromCodexContent(payload.message);
|
|
2941
|
+
if (text) {
|
|
2942
|
+
pushMessage({
|
|
2943
|
+
role: "assistant",
|
|
2944
|
+
text,
|
|
2945
|
+
...typeof event.timestamp === "string" ? { occurredAt: event.timestamp } : {}
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
if (event.type === "response_item" && payload?.type === "message" && (payload.role === "user" || payload.role === "assistant")) {
|
|
2950
|
+
const text = stringFromCodexContent(payload.content) ?? stringFromCodexContent(payload.message);
|
|
2951
|
+
if (text) {
|
|
2952
|
+
pushMessage({
|
|
2953
|
+
role: payload.role,
|
|
2954
|
+
text,
|
|
2955
|
+
...typeof event.timestamp === "string" ? { occurredAt: event.timestamp } : {}
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
const turns = [];
|
|
2961
|
+
let pendingUser;
|
|
2962
|
+
let pendingAssistantParts = [];
|
|
2963
|
+
const flushPendingTurn = () => {
|
|
2964
|
+
if (!pendingUser) {
|
|
2965
|
+
return;
|
|
2966
|
+
}
|
|
2967
|
+
const assistant = pendingAssistantParts.join(`
|
|
2968
|
+
|
|
2969
|
+
`).trim();
|
|
2970
|
+
turns.push({
|
|
2971
|
+
user: truncateText(pendingUser.text, 6000),
|
|
2972
|
+
...assistant ? { assistant: truncateText(assistant, 6000) } : {},
|
|
2973
|
+
...pendingUser.occurredAt ? { occurredAt: pendingUser.occurredAt } : {}
|
|
2974
|
+
});
|
|
2975
|
+
pendingUser = undefined;
|
|
2976
|
+
pendingAssistantParts = [];
|
|
2977
|
+
};
|
|
2978
|
+
for (const message of messages) {
|
|
2979
|
+
if (message.role === "user") {
|
|
2980
|
+
flushPendingTurn();
|
|
2981
|
+
pendingUser = {
|
|
2982
|
+
text: message.text,
|
|
2983
|
+
...message.occurredAt ? { occurredAt: message.occurredAt } : {}
|
|
2984
|
+
};
|
|
2985
|
+
pendingAssistantParts = [];
|
|
2986
|
+
continue;
|
|
2987
|
+
}
|
|
2988
|
+
if (pendingUser) {
|
|
2989
|
+
if (pendingAssistantParts[pendingAssistantParts.length - 1] !== message.text) {
|
|
2990
|
+
pendingAssistantParts.push(message.text);
|
|
2991
|
+
}
|
|
2992
|
+
} else {
|
|
2993
|
+
turns.push({
|
|
2994
|
+
assistant: truncateText(message.text, 6000),
|
|
2995
|
+
...message.occurredAt ? { occurredAt: message.occurredAt } : {}
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
flushPendingTurn();
|
|
3000
|
+
return turns.length ? turns.slice(-3) : undefined;
|
|
3001
|
+
}
|
|
3002
|
+
function readRecentTurnsFromCodexSession(agentSessionId, options) {
|
|
3003
|
+
const sessionsDirectory = codexSessionsDirectory(options);
|
|
3004
|
+
if (!sessionsDirectory) {
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
3007
|
+
const sessionFile = findCodexSessionFile(sessionsDirectory, agentSessionId);
|
|
3008
|
+
return sessionFile ? readRecentTurnsFromCodexSessionFile(sessionFile) : undefined;
|
|
3009
|
+
}
|
|
2582
3010
|
function normalizeAgentEvent(input, parsed, options) {
|
|
2583
3011
|
const source = parseAgentEventSource(parsed.options.source);
|
|
2584
3012
|
const event = parsed.options.event ?? "stop";
|
|
2585
3013
|
const base = normalizeAgentEventBase(input, source, event, options);
|
|
2586
3014
|
switch (source) {
|
|
2587
3015
|
case "codex":
|
|
2588
|
-
return normalizeCodexAgentEvent(input, base);
|
|
3016
|
+
return normalizeCodexAgentEvent(input, base, options);
|
|
2589
3017
|
case "claude-code":
|
|
2590
3018
|
case "opencode":
|
|
2591
3019
|
case "openrouter":
|
|
@@ -2596,6 +3024,7 @@ function normalizeAgentEvent(input, parsed, options) {
|
|
|
2596
3024
|
function normalizeAgentEventBase(input, source, event, options) {
|
|
2597
3025
|
const threadId = firstString(input, [["thread_id"], ["threadId"], ["conversation_id"], ["conversationId"]]);
|
|
2598
3026
|
const turnId = firstString(input, [["turn_id"], ["turnId"], ["id"]]);
|
|
3027
|
+
const recentTurns = readRecentAgentTurns(input);
|
|
2599
3028
|
const lastAssistantMessage = firstString(input, [
|
|
2600
3029
|
["last_assistant_message"],
|
|
2601
3030
|
["lastAssistantMessage"],
|
|
@@ -2612,10 +3041,11 @@ function normalizeAgentEventBase(input, source, event, options) {
|
|
|
2612
3041
|
...threadId ? { threadId } : {},
|
|
2613
3042
|
...turnId ? { turnId } : {},
|
|
2614
3043
|
...lastAssistantMessage ? { lastAssistantMessage: truncateText(lastAssistantMessage, 6000) } : {},
|
|
3044
|
+
...recentTurns ? { recentTurns } : {},
|
|
2615
3045
|
payloadKeys: Object.keys(input).sort()
|
|
2616
3046
|
};
|
|
2617
3047
|
}
|
|
2618
|
-
function normalizeCodexAgentEvent(input, base) {
|
|
3048
|
+
function normalizeCodexAgentEvent(input, base, options) {
|
|
2619
3049
|
const agentSessionId = firstString(input, [["session_id"], ["sessionId"], ["session", "id"]]);
|
|
2620
3050
|
if (!agentSessionId) {
|
|
2621
3051
|
return {
|
|
@@ -2623,10 +3053,13 @@ function normalizeCodexAgentEvent(input, base) {
|
|
|
2623
3053
|
diagnosticReason: "missing_agent_session_id"
|
|
2624
3054
|
};
|
|
2625
3055
|
}
|
|
3056
|
+
const transcriptPath = firstString(input, [["transcript_path"], ["transcriptPath"]]);
|
|
3057
|
+
const recentTurns = base.recentTurns?.length ? base.recentTurns : transcriptPath ? readRecentTurnsFromCodexSessionFile(transcriptPath) : readRecentTurnsFromCodexSession(agentSessionId, options);
|
|
2626
3058
|
return {
|
|
2627
3059
|
...base,
|
|
2628
3060
|
agentSessionId,
|
|
2629
|
-
agentSessionKey: agentSessionKey(base.source, agentSessionId)
|
|
3061
|
+
agentSessionKey: agentSessionKey(base.source, agentSessionId),
|
|
3062
|
+
...recentTurns?.length ? { recentTurns } : {}
|
|
2630
3063
|
};
|
|
2631
3064
|
}
|
|
2632
3065
|
function normalizeGenericAgentEvent(input, base) {
|
|
@@ -2652,14 +3085,23 @@ function normalizeGenericAgentEvent(input, base) {
|
|
|
2652
3085
|
function agentSessionKey(source, agentSessionId) {
|
|
2653
3086
|
return `${source}:${stableHash(agentSessionId)}`;
|
|
2654
3087
|
}
|
|
3088
|
+
function checkpointWorkLogMetadata(settings, tokenAccounting) {
|
|
3089
|
+
return {
|
|
3090
|
+
checkpoint: {
|
|
3091
|
+
provider: settings.provider,
|
|
3092
|
+
...settings.model ? { model: settings.model } : {},
|
|
3093
|
+
...tokenAccounting ? { tokenAccounting } : {}
|
|
3094
|
+
}
|
|
3095
|
+
};
|
|
3096
|
+
}
|
|
2655
3097
|
function defaultAgentLoopState() {
|
|
2656
3098
|
return {
|
|
2657
3099
|
version: 2,
|
|
2658
3100
|
sessions: {}
|
|
2659
3101
|
};
|
|
2660
3102
|
}
|
|
2661
|
-
function
|
|
2662
|
-
return
|
|
3103
|
+
function defaultCheckpointThresholdValue() {
|
|
3104
|
+
return defaultCheckpointThreshold;
|
|
2663
3105
|
}
|
|
2664
3106
|
function agentLoopDirectory(parsed, options) {
|
|
2665
3107
|
return parsed.options["state-dir"] ? isAbsolute(parsed.options["state-dir"]) ? parsed.options["state-dir"] : join(options.cwd ?? process.cwd(), parsed.options["state-dir"]) : join(options.cwd ?? process.cwd(), ".context", "cadence-agent-loop");
|
|
@@ -2667,8 +3109,81 @@ function agentLoopDirectory(parsed, options) {
|
|
|
2667
3109
|
function agentLoopStatePath(parsed, options) {
|
|
2668
3110
|
return join(agentLoopDirectory(parsed, options), "state.json");
|
|
2669
3111
|
}
|
|
3112
|
+
function agentLoopSettingsPath(parsed, options) {
|
|
3113
|
+
if (parsed.options["state-dir"]) {
|
|
3114
|
+
return join(agentLoopDirectory(parsed, options), "settings.json");
|
|
3115
|
+
}
|
|
3116
|
+
return join(getConfigHome(options.env ?? process.env), "agent-run", "settings.json");
|
|
3117
|
+
}
|
|
3118
|
+
function legacyAgentLoopSettingsPath(parsed, options) {
|
|
3119
|
+
return join(agentLoopDirectory(parsed, options), "settings.json");
|
|
3120
|
+
}
|
|
2670
3121
|
function agentLoopLockPath(parsed, options, agentSessionKeyValue) {
|
|
2671
|
-
return join(agentLoopDirectory(parsed, options), agentSessionKeyValue ? `
|
|
3122
|
+
return join(agentLoopDirectory(parsed, options), agentSessionKeyValue ? `checkpoint-${stableHash(agentSessionKeyValue)}.lock` : "checkpoint.lock");
|
|
3123
|
+
}
|
|
3124
|
+
function normalizeCheckpointModel(value) {
|
|
3125
|
+
const trimmed = value?.trim();
|
|
3126
|
+
if (!trimmed) {
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
3129
|
+
if (trimmed.length > 120 || /\s/.test(trimmed)) {
|
|
3130
|
+
throw new CliError("CLI_USAGE", "Checkpoint model must be a non-empty model id without whitespace.");
|
|
3131
|
+
}
|
|
3132
|
+
return trimmed;
|
|
3133
|
+
}
|
|
3134
|
+
function defaultAgentLoopSettings() {
|
|
3135
|
+
return {
|
|
3136
|
+
version: 1,
|
|
3137
|
+
checkpoint: {
|
|
3138
|
+
provider: "codex"
|
|
3139
|
+
}
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
async function readAgentLoopSettings(parsed, options) {
|
|
3143
|
+
const filePath = agentLoopSettingsPath(parsed, options);
|
|
3144
|
+
let file = Bun.file(filePath);
|
|
3145
|
+
if (!await file.exists()) {
|
|
3146
|
+
const legacyFilePath = legacyAgentLoopSettingsPath(parsed, options);
|
|
3147
|
+
if (legacyFilePath === filePath) {
|
|
3148
|
+
return defaultAgentLoopSettings();
|
|
3149
|
+
}
|
|
3150
|
+
file = Bun.file(legacyFilePath);
|
|
3151
|
+
if (!await file.exists()) {
|
|
3152
|
+
return defaultAgentLoopSettings();
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
const parsedSettings = JSON.parse(await file.text());
|
|
3156
|
+
if (!parsedSettings || typeof parsedSettings !== "object" || Array.isArray(parsedSettings)) {
|
|
3157
|
+
return defaultAgentLoopSettings();
|
|
3158
|
+
}
|
|
3159
|
+
const record = parsedSettings;
|
|
3160
|
+
const checkpoint = record.checkpoint;
|
|
3161
|
+
if (!checkpoint || typeof checkpoint !== "object" || Array.isArray(checkpoint)) {
|
|
3162
|
+
return defaultAgentLoopSettings();
|
|
3163
|
+
}
|
|
3164
|
+
const checkpointRecord = checkpoint;
|
|
3165
|
+
const provider = checkpointRecord.provider === "codex" ? "codex" : "codex";
|
|
3166
|
+
const model = typeof checkpointRecord.model === "string" ? normalizeCheckpointModel(checkpointRecord.model) : undefined;
|
|
3167
|
+
return {
|
|
3168
|
+
version: 1,
|
|
3169
|
+
checkpoint: {
|
|
3170
|
+
provider,
|
|
3171
|
+
...model ? { model } : {},
|
|
3172
|
+
...typeof checkpointRecord.updatedAt === "string" ? { updatedAt: checkpointRecord.updatedAt } : {}
|
|
3173
|
+
}
|
|
3174
|
+
};
|
|
3175
|
+
}
|
|
3176
|
+
async function resolveCheckpointSettings(parsed, options) {
|
|
3177
|
+
const saved = await readAgentLoopSettings(parsed, options);
|
|
3178
|
+
const provider = parsed.options["checkpoint-provider"] ?? saved.checkpoint.provider;
|
|
3179
|
+
if (provider !== "codex") {
|
|
3180
|
+
throw new CliError("CLI_USAGE", "Only the Codex checkpoint provider is supported in this version.");
|
|
3181
|
+
}
|
|
3182
|
+
const model = normalizeCheckpointModel(parsed.options["checkpoint-model"] ?? parsed.options.model ?? saved.checkpoint.model);
|
|
3183
|
+
return {
|
|
3184
|
+
provider: "codex",
|
|
3185
|
+
...model ? { model } : {}
|
|
3186
|
+
};
|
|
2672
3187
|
}
|
|
2673
3188
|
async function readAgentLoopState(parsed, options) {
|
|
2674
3189
|
const filePath = agentLoopStatePath(parsed, options);
|
|
@@ -2714,24 +3229,45 @@ function readAgentLoopSessions(rawSessions) {
|
|
|
2714
3229
|
const record = rawSession;
|
|
2715
3230
|
const source = parseAgentEventSource(typeof record.source === "string" ? record.source : undefined);
|
|
2716
3231
|
const stopCount = typeof record.stopCount === "number" && Number.isInteger(record.stopCount) && record.stopCount >= 0 ? record.stopCount : 0;
|
|
2717
|
-
const threshold = typeof record.threshold === "number" && Number.isInteger(record.threshold) && record.threshold > 0 ? record.threshold :
|
|
3232
|
+
const threshold = typeof record.threshold === "number" && Number.isInteger(record.threshold) && record.threshold > 0 ? record.threshold : defaultCheckpointThresholdValue();
|
|
3233
|
+
const recentTurns = Array.isArray(record.recentTurns) ? readRecentAgentTurns({
|
|
3234
|
+
recentTurns: record.recentTurns
|
|
3235
|
+
}) : undefined;
|
|
2718
3236
|
sessions[key] = {
|
|
2719
3237
|
source,
|
|
2720
3238
|
stopCount,
|
|
2721
3239
|
threshold,
|
|
3240
|
+
...readAgentSessionCadenceContext(record),
|
|
2722
3241
|
...typeof record.firstObservedAt === "string" ? { firstObservedAt: record.firstObservedAt } : {},
|
|
2723
3242
|
...typeof record.lastObservedAt === "string" ? { lastObservedAt: record.lastObservedAt } : {},
|
|
3243
|
+
...typeof record.lastObservedTurnId === "string" ? { lastObservedTurnId: record.lastObservedTurnId } : {},
|
|
2724
3244
|
...typeof record.lastAction === "string" ? { lastAction: record.lastAction } : {},
|
|
2725
3245
|
...typeof record.lastReason === "string" ? { lastReason: record.lastReason } : {},
|
|
2726
3246
|
...typeof record.lastEventFile === "string" ? { lastEventFile: record.lastEventFile } : {},
|
|
2727
3247
|
...typeof record.lastAssistantMessage === "string" ? { lastAssistantMessage: record.lastAssistantMessage } : {},
|
|
3248
|
+
...recentTurns ? { recentTurns } : {},
|
|
2728
3249
|
...typeof record.lastCheckpointAt === "string" ? { lastCheckpointAt: record.lastCheckpointAt } : {},
|
|
2729
3250
|
...typeof record.lastCheckpointFingerprint === "string" ? { lastCheckpointFingerprint: record.lastCheckpointFingerprint } : {},
|
|
2730
|
-
...typeof record.previousCheckpointSummary === "string" ? { previousCheckpointSummary: record.previousCheckpointSummary } : {}
|
|
3251
|
+
...typeof record.previousCheckpointSummary === "string" ? { previousCheckpointSummary: record.previousCheckpointSummary } : {},
|
|
3252
|
+
...typeof record.lastCheckpointAuditFile === "string" ? { lastCheckpointAuditFile: record.lastCheckpointAuditFile } : {}
|
|
2731
3253
|
};
|
|
2732
3254
|
}
|
|
2733
3255
|
return sessions;
|
|
2734
3256
|
}
|
|
3257
|
+
function readAgentSessionCadenceContext(record) {
|
|
3258
|
+
const value = record.cadenceContext;
|
|
3259
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3260
|
+
return {};
|
|
3261
|
+
}
|
|
3262
|
+
const contextRecord = value;
|
|
3263
|
+
const cadenceContext = {
|
|
3264
|
+
...typeof contextRecord.ticketId === "string" ? { ticketId: contextRecord.ticketId } : {},
|
|
3265
|
+
...typeof contextRecord.sessionId === "string" ? { sessionId: contextRecord.sessionId } : {},
|
|
3266
|
+
...typeof contextRecord.changesetId === "string" ? { changesetId: contextRecord.changesetId } : {},
|
|
3267
|
+
...typeof contextRecord.capturedAt === "string" ? { capturedAt: contextRecord.capturedAt } : {}
|
|
3268
|
+
};
|
|
3269
|
+
return cadenceContext.ticketId || cadenceContext.sessionId || cadenceContext.changesetId ? { cadenceContext } : {};
|
|
3270
|
+
}
|
|
2735
3271
|
function readAgentLoopDiagnostics(record) {
|
|
2736
3272
|
return {
|
|
2737
3273
|
...typeof record.missingSessionIdCount === "number" && Number.isInteger(record.missingSessionIdCount) && record.missingSessionIdCount > 0 ? { missingSessionIdCount: record.missingSessionIdCount } : {},
|
|
@@ -2766,6 +3302,15 @@ async function writeAgentEventFile(parsed, options, event) {
|
|
|
2766
3302
|
`);
|
|
2767
3303
|
return filePath;
|
|
2768
3304
|
}
|
|
3305
|
+
async function writeAgentCheckpointAuditFile(parsed, options, audit) {
|
|
3306
|
+
const checkpointsDirectory = join(agentLoopDirectory(parsed, options), "checkpoints");
|
|
3307
|
+
const fileName = `${new Date().toISOString().replace(/[:.]/g, "-")}-${randomUUID()}.json`;
|
|
3308
|
+
const filePath = join(checkpointsDirectory, fileName);
|
|
3309
|
+
await mkdir(checkpointsDirectory, { recursive: true });
|
|
3310
|
+
await writeFile(filePath, `${JSON.stringify(audit, null, 2)}
|
|
3311
|
+
`);
|
|
3312
|
+
return filePath;
|
|
3313
|
+
}
|
|
2769
3314
|
async function removeStaleAgentLoopLock(lockPath) {
|
|
2770
3315
|
try {
|
|
2771
3316
|
const lockStats = await stat(lockPath);
|
|
@@ -2851,7 +3396,7 @@ function currentCliWorkerInvocation() {
|
|
|
2851
3396
|
argsPrefix: []
|
|
2852
3397
|
};
|
|
2853
3398
|
}
|
|
2854
|
-
async function
|
|
3399
|
+
async function spawnAgentRunCheckpoint(args, options) {
|
|
2855
3400
|
const cwd = options.cwd ?? process.cwd();
|
|
2856
3401
|
const invocation = currentCliWorkerInvocation();
|
|
2857
3402
|
const env = {
|
|
@@ -2870,47 +3415,88 @@ async function spawnAgentRunCloseout(args, options) {
|
|
|
2870
3415
|
});
|
|
2871
3416
|
child.unref();
|
|
2872
3417
|
}
|
|
3418
|
+
function currentRecordTime(value, keys) {
|
|
3419
|
+
for (const key of keys) {
|
|
3420
|
+
const candidate = value[key];
|
|
3421
|
+
if (typeof candidate !== "string") {
|
|
3422
|
+
continue;
|
|
3423
|
+
}
|
|
3424
|
+
const time = new Date(candidate).getTime();
|
|
3425
|
+
if (!Number.isNaN(time)) {
|
|
3426
|
+
return time;
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
return 0;
|
|
3430
|
+
}
|
|
2873
3431
|
function activeSessionFromCurrent(current) {
|
|
2874
3432
|
const record = current && typeof current === "object" ? current : null;
|
|
2875
3433
|
const sessions = Array.isArray(record?.sessions) ? record.sessions : [];
|
|
2876
|
-
const activeSession = sessions.find((session) => {
|
|
2877
|
-
if (!session || typeof session !== "object") {
|
|
2878
|
-
return false;
|
|
2879
|
-
}
|
|
2880
|
-
const sessionRecord2 = session;
|
|
2881
|
-
return sessionRecord2.status === "active" && typeof sessionRecord2.ticketId === "string";
|
|
2882
|
-
});
|
|
2883
|
-
if (activeSession && typeof activeSession === "object") {
|
|
2884
|
-
return activeSession;
|
|
2885
|
-
}
|
|
2886
3434
|
const activeLeases = Array.isArray(record?.activeLeases) ? record.activeLeases : Array.isArray(record?.leases) ? record.leases.filter((lease) => lease && typeof lease === "object" && lease.status === "active") : [];
|
|
2887
|
-
const activeLease = activeLeases.
|
|
3435
|
+
const activeLease = activeLeases.filter((lease) => {
|
|
2888
3436
|
if (!lease || typeof lease !== "object") {
|
|
2889
3437
|
return false;
|
|
2890
3438
|
}
|
|
2891
|
-
const
|
|
2892
|
-
return typeof
|
|
2893
|
-
});
|
|
2894
|
-
if (
|
|
2895
|
-
|
|
3439
|
+
const leaseRecord = lease;
|
|
3440
|
+
return typeof leaseRecord.ticketId === "string";
|
|
3441
|
+
}).map((lease) => lease).sort((left, right) => currentRecordTime(right, ["lastSeenAt", "claimedAt", "expiresAt"]) - currentRecordTime(left, ["lastSeenAt", "claimedAt", "expiresAt"]))[0];
|
|
3442
|
+
if (activeLease && typeof activeLease === "object") {
|
|
3443
|
+
const leaseRecord = activeLease;
|
|
3444
|
+
const sessionId = typeof leaseRecord.sessionId === "string" ? leaseRecord.sessionId : undefined;
|
|
3445
|
+
const matchingSession = sessions.find((session) => {
|
|
3446
|
+
if (!session || typeof session !== "object") {
|
|
3447
|
+
return false;
|
|
3448
|
+
}
|
|
3449
|
+
return sessionId && session.id === sessionId;
|
|
3450
|
+
});
|
|
3451
|
+
const sessionRecord = matchingSession && typeof matchingSession === "object" ? matchingSession : {};
|
|
3452
|
+
return {
|
|
3453
|
+
...sessionRecord,
|
|
3454
|
+
...sessionId ? { id: sessionId } : {},
|
|
3455
|
+
ticketId: leaseRecord.ticketId,
|
|
3456
|
+
...typeof leaseRecord.changesetId === "string" ? { changesetId: leaseRecord.changesetId } : typeof sessionRecord.changesetId === "string" ? { changesetId: sessionRecord.changesetId } : {},
|
|
3457
|
+
status: "active"
|
|
3458
|
+
};
|
|
2896
3459
|
}
|
|
2897
|
-
const
|
|
2898
|
-
const sessionId = typeof leaseRecord.sessionId === "string" ? leaseRecord.sessionId : undefined;
|
|
2899
|
-
const matchingSession = sessions.find((session) => {
|
|
3460
|
+
const activeSessions = sessions.filter((session) => {
|
|
2900
3461
|
if (!session || typeof session !== "object") {
|
|
2901
3462
|
return false;
|
|
2902
3463
|
}
|
|
2903
|
-
|
|
3464
|
+
const sessionRecord = session;
|
|
3465
|
+
return sessionRecord.status === "active" && typeof sessionRecord.ticketId === "string";
|
|
2904
3466
|
});
|
|
2905
|
-
const
|
|
3467
|
+
const activeSession = activeSessions.map((session) => session).sort((left, right) => currentRecordTime(right, ["lastActivityAt", "startedAt"]) - currentRecordTime(left, ["lastActivityAt", "startedAt"]))[0];
|
|
3468
|
+
if (activeSession && typeof activeSession === "object") {
|
|
3469
|
+
return activeSession;
|
|
3470
|
+
}
|
|
3471
|
+
return null;
|
|
3472
|
+
}
|
|
3473
|
+
function cadenceContextFromCurrent(current) {
|
|
3474
|
+
const activeSession = activeSessionFromCurrent(current);
|
|
3475
|
+
if (!activeSession) {
|
|
3476
|
+
return;
|
|
3477
|
+
}
|
|
3478
|
+
const ticketId = typeof activeSession.ticketId === "string" ? activeSession.ticketId : undefined;
|
|
3479
|
+
const sessionId = typeof activeSession.id === "string" ? activeSession.id : undefined;
|
|
3480
|
+
const changesetId = typeof activeSession.changesetId === "string" ? activeSession.changesetId : undefined;
|
|
3481
|
+
if (!ticketId && !sessionId && !changesetId) {
|
|
3482
|
+
return;
|
|
3483
|
+
}
|
|
2906
3484
|
return {
|
|
2907
|
-
...
|
|
2908
|
-
...sessionId ? {
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
status: "active"
|
|
3485
|
+
...ticketId ? { ticketId } : {},
|
|
3486
|
+
...sessionId ? { sessionId } : {},
|
|
3487
|
+
...changesetId ? { changesetId } : {},
|
|
3488
|
+
capturedAt: new Date().toISOString()
|
|
2912
3489
|
};
|
|
2913
3490
|
}
|
|
3491
|
+
async function readCurrentCadenceContext(client, projectId) {
|
|
3492
|
+
const current = await client.sessions.current({
|
|
3493
|
+
projectId,
|
|
3494
|
+
filters: {
|
|
3495
|
+
limit: 100
|
|
3496
|
+
}
|
|
3497
|
+
});
|
|
3498
|
+
return cadenceContextFromCurrent(current);
|
|
3499
|
+
}
|
|
2914
3500
|
function fingerprintForCheckpoint(event, options) {
|
|
2915
3501
|
const status = gitOutput(["status", "--short"], options);
|
|
2916
3502
|
const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
|
|
@@ -2918,6 +3504,7 @@ function fingerprintForCheckpoint(event, options) {
|
|
|
2918
3504
|
source: event.source,
|
|
2919
3505
|
event: event.event,
|
|
2920
3506
|
lastAssistantMessage: event.lastAssistantMessage ?? "",
|
|
3507
|
+
recentTurns: event.recentTurns ?? [],
|
|
2921
3508
|
status,
|
|
2922
3509
|
changedFiles
|
|
2923
3510
|
}));
|
|
@@ -2932,60 +3519,486 @@ function shouldSkipForCooldown(state, cooldownSeconds) {
|
|
|
2932
3519
|
function synthesizeAgentEventFromSession(agentSessionKeyValue, session, options) {
|
|
2933
3520
|
return {
|
|
2934
3521
|
source: session.source,
|
|
2935
|
-
event: "
|
|
3522
|
+
event: "checkpoint",
|
|
2936
3523
|
workspacePath: options.cwd ?? process.cwd(),
|
|
2937
3524
|
occurredAt: new Date().toISOString(),
|
|
2938
3525
|
agentSessionKey: agentSessionKeyValue,
|
|
2939
3526
|
...session.lastAssistantMessage ? { lastAssistantMessage: session.lastAssistantMessage } : {},
|
|
3527
|
+
...session.recentTurns?.length ? { recentTurns: session.recentTurns } : {},
|
|
2940
3528
|
payloadKeys: []
|
|
2941
3529
|
};
|
|
2942
3530
|
}
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
3531
|
+
var checkpointRouteActions = ["current", "noop", "intake_create", "intake_attach", "switch_existing", "needs_human"];
|
|
3532
|
+
var checkpointConfidenceLevels = ["low", "medium", "high"];
|
|
3533
|
+
var checkpointSessionActions = ["keep", "handoff", "end", "complete_ticket"];
|
|
3534
|
+
var highAutonomyRouteActions = new Set(["intake_create", "intake_attach", "switch_existing"]);
|
|
3535
|
+
var checkpointServerTextLimit = 1200;
|
|
3536
|
+
function checkpointEntrySummary(body) {
|
|
3537
|
+
return truncateText(body.replace(/\s+/g, " ").trim(), 500);
|
|
3538
|
+
}
|
|
3539
|
+
function checkpointString(record, keys) {
|
|
3540
|
+
for (const key of keys) {
|
|
3541
|
+
const value = record[key];
|
|
3542
|
+
if (typeof value === "string" && value.trim()) {
|
|
3543
|
+
return value.trim();
|
|
2953
3544
|
}
|
|
3545
|
+
}
|
|
3546
|
+
return;
|
|
3547
|
+
}
|
|
3548
|
+
function parseCheckpointParent(value) {
|
|
3549
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
3550
|
+
return {};
|
|
3551
|
+
}
|
|
3552
|
+
try {
|
|
3553
|
+
return parseWorkLogParent(value.trim());
|
|
2954
3554
|
} catch {
|
|
2955
|
-
|
|
2956
|
-
const end = trimmed.lastIndexOf("}");
|
|
2957
|
-
if (start >= 0 && end > start) {
|
|
2958
|
-
try {
|
|
2959
|
-
const parsed = JSON.parse(trimmed.slice(start, end + 1));
|
|
2960
|
-
if (parsed && typeof parsed === "object") {
|
|
2961
|
-
const record = parsed;
|
|
2962
|
-
return {
|
|
2963
|
-
body: typeof record.body === "string" && record.body.trim() ? record.body.trim() : trimmed,
|
|
2964
|
-
summary: typeof record.summary === "string" && record.summary.trim() ? record.summary.trim() : undefined
|
|
2965
|
-
};
|
|
2966
|
-
}
|
|
2967
|
-
} catch {}
|
|
2968
|
-
}
|
|
3555
|
+
return {};
|
|
2969
3556
|
}
|
|
2970
|
-
return {
|
|
2971
|
-
body: trimmed,
|
|
2972
|
-
summary: undefined
|
|
2973
|
-
};
|
|
2974
3557
|
}
|
|
2975
|
-
function
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3558
|
+
function parseCheckpointEntry(value, fallbackKind) {
|
|
3559
|
+
if (typeof value === "string" && value.trim()) {
|
|
3560
|
+
const body2 = value.trim();
|
|
3561
|
+
return {
|
|
3562
|
+
kind: fallbackKind,
|
|
3563
|
+
body: body2,
|
|
3564
|
+
summary: checkpointEntrySummary(body2)
|
|
3565
|
+
};
|
|
3566
|
+
}
|
|
3567
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3568
|
+
return;
|
|
3569
|
+
}
|
|
3570
|
+
const record = value;
|
|
3571
|
+
const body = checkpointString(record, ["body", "text", "content"]);
|
|
3572
|
+
if (!body) {
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3575
|
+
let kind = fallbackKind;
|
|
3576
|
+
const rawKind = checkpointString(record, ["kind", "entryKind", "type"]);
|
|
3577
|
+
if (rawKind && workLogEntryKinds.includes(rawKind)) {
|
|
3578
|
+
kind = rawKind;
|
|
3579
|
+
}
|
|
3580
|
+
const parent = parseCheckpointParent(record.under ?? record.parent ?? record.parentSelector ?? record.parentEntryId);
|
|
3581
|
+
const summary = checkpointString(record, ["summary", "title"]);
|
|
3582
|
+
return {
|
|
3583
|
+
kind,
|
|
3584
|
+
body,
|
|
3585
|
+
summary: summary ?? checkpointEntrySummary(body),
|
|
3586
|
+
...parent
|
|
3587
|
+
};
|
|
3588
|
+
}
|
|
3589
|
+
function parseCheckpointSummaryUpdate(value) {
|
|
3590
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3591
|
+
return;
|
|
3592
|
+
}
|
|
3593
|
+
const record = value;
|
|
3594
|
+
const update = record.update === true;
|
|
3595
|
+
const summary = checkpointString(record, ["value", "summary", "currentSummary"]);
|
|
3596
|
+
const reason = checkpointString(record, ["reason"]);
|
|
3597
|
+
return {
|
|
3598
|
+
update,
|
|
3599
|
+
...summary ? { value: summary } : {},
|
|
3600
|
+
...reason ? { reason } : {}
|
|
3601
|
+
};
|
|
3602
|
+
}
|
|
3603
|
+
function parseCheckpointRecord(record, rawText, fallbackKind) {
|
|
3604
|
+
const rawEntries = Array.isArray(record.entries) ? record.entries : [];
|
|
3605
|
+
const entries = rawEntries.map((entry) => parseCheckpointEntry(entry, fallbackKind)).filter((entry) => Boolean(entry));
|
|
3606
|
+
const legacyBody = checkpointString(record, ["body"]);
|
|
3607
|
+
const summary = checkpointString(record, ["summary"]);
|
|
3608
|
+
const parsedEntries = entries.length > 0 ? entries : legacyBody ? [parseCheckpointEntry({ kind: fallbackKind, body: legacyBody, summary }, fallbackKind)].filter((entry) => Boolean(entry)) : [];
|
|
3609
|
+
const currentSummary = parseCheckpointSummaryUpdate(record.currentSummary);
|
|
3610
|
+
return {
|
|
3611
|
+
summary: summary ?? parsedEntries[0]?.summary ?? checkpointEntrySummary(rawText),
|
|
3612
|
+
entries: parsedEntries,
|
|
3613
|
+
...currentSummary ? { currentSummary } : {}
|
|
3614
|
+
};
|
|
3615
|
+
}
|
|
3616
|
+
function parseCheckpointJson(raw, fallbackKind) {
|
|
3617
|
+
const trimmed = raw.trim();
|
|
3618
|
+
try {
|
|
3619
|
+
const parsed = JSON.parse(trimmed);
|
|
3620
|
+
if (parsed && typeof parsed === "object") {
|
|
3621
|
+
return parseCheckpointRecord(parsed, trimmed, fallbackKind);
|
|
3622
|
+
}
|
|
3623
|
+
} catch {
|
|
3624
|
+
const start = trimmed.indexOf("{");
|
|
3625
|
+
const end = trimmed.lastIndexOf("}");
|
|
3626
|
+
if (start >= 0 && end > start) {
|
|
3627
|
+
try {
|
|
3628
|
+
const parsed = JSON.parse(trimmed.slice(start, end + 1));
|
|
3629
|
+
if (parsed && typeof parsed === "object") {
|
|
3630
|
+
return parseCheckpointRecord(parsed, trimmed, fallbackKind);
|
|
3631
|
+
}
|
|
3632
|
+
} catch {}
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
return {
|
|
3636
|
+
summary: checkpointEntrySummary(trimmed),
|
|
3637
|
+
entries: trimmed ? [
|
|
3638
|
+
{
|
|
3639
|
+
kind: fallbackKind,
|
|
3640
|
+
body: trimmed,
|
|
3641
|
+
summary: checkpointEntrySummary(trimmed)
|
|
3642
|
+
}
|
|
3643
|
+
] : []
|
|
3644
|
+
};
|
|
3645
|
+
}
|
|
3646
|
+
function checkpointValidationError(message, details) {
|
|
3647
|
+
return new CliError("AGENT_RUN_CHECKPOINT_INVALID_PLAN", message, details);
|
|
3648
|
+
}
|
|
3649
|
+
function checkpointPlanString(record, key, limit = checkpointServerTextLimit) {
|
|
3650
|
+
const value = record[key];
|
|
3651
|
+
if (typeof value !== "string") {
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
3654
|
+
const trimmed = value.trim();
|
|
3655
|
+
if (!trimmed) {
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
3658
|
+
if (trimmed.length > limit) {
|
|
3659
|
+
throw checkpointValidationError(`Checkpoint plan field "${key}" is too long.`, { key, limit });
|
|
3660
|
+
}
|
|
3661
|
+
return trimmed;
|
|
3662
|
+
}
|
|
3663
|
+
function parseCheckpointRoute(value) {
|
|
3664
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3665
|
+
throw checkpointValidationError("Checkpoint plan requires route.");
|
|
3666
|
+
}
|
|
3667
|
+
const record = value;
|
|
3668
|
+
const actionValue = checkpointPlanString(record, "action", 80);
|
|
3669
|
+
const confidenceValue = checkpointPlanString(record, "confidence", 20) ?? "medium";
|
|
3670
|
+
if (!actionValue || !checkpointRouteActions.includes(actionValue)) {
|
|
3671
|
+
throw checkpointValidationError("Checkpoint plan route.action is invalid.", { action: actionValue });
|
|
3672
|
+
}
|
|
3673
|
+
if (!checkpointConfidenceLevels.includes(confidenceValue)) {
|
|
3674
|
+
throw checkpointValidationError("Checkpoint plan route.confidence is invalid.", { confidence: confidenceValue });
|
|
3675
|
+
}
|
|
3676
|
+
const reason = checkpointPlanString(record, "reason");
|
|
3677
|
+
const request = checkpointPlanString(record, "request");
|
|
3678
|
+
const targetTicketId = checkpointPlanString(record, "targetTicketId", 100);
|
|
3679
|
+
return {
|
|
3680
|
+
action: actionValue,
|
|
3681
|
+
confidence: confidenceValue,
|
|
3682
|
+
...reason ? { reason } : {},
|
|
3683
|
+
...request ? { request } : {},
|
|
3684
|
+
...targetTicketId ? { targetTicketId } : {}
|
|
3685
|
+
};
|
|
3686
|
+
}
|
|
3687
|
+
function parseCheckpointPlanEntry(value) {
|
|
3688
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3689
|
+
throw checkpointValidationError("Checkpoint plan entries must be objects.");
|
|
3690
|
+
}
|
|
3691
|
+
const record = value;
|
|
3692
|
+
const rawKind = checkpointPlanString(record, "kind", 40) ?? checkpointPlanString(record, "entryKind", 40) ?? checkpointPlanString(record, "type", 40);
|
|
3693
|
+
const body = checkpointPlanString(record, "body") ?? checkpointPlanString(record, "text") ?? checkpointPlanString(record, "content");
|
|
3694
|
+
if (!rawKind || !workLogEntryKinds.includes(rawKind)) {
|
|
3695
|
+
throw checkpointValidationError("Checkpoint plan entry kind is invalid.", { kind: rawKind });
|
|
3696
|
+
}
|
|
3697
|
+
if (!body) {
|
|
3698
|
+
throw checkpointValidationError("Checkpoint plan entry body is required.");
|
|
3699
|
+
}
|
|
3700
|
+
const parent = parseCheckpointParent(record.under ?? record.parent ?? record.parentSelector ?? record.parentEntryId);
|
|
3701
|
+
const summary = checkpointPlanString(record, "summary", 300) ?? checkpointPlanString(record, "title", 300);
|
|
3702
|
+
return {
|
|
3703
|
+
kind: rawKind,
|
|
3704
|
+
body,
|
|
3705
|
+
summary: summary ?? checkpointEntrySummary(body),
|
|
3706
|
+
...parent
|
|
3707
|
+
};
|
|
3708
|
+
}
|
|
3709
|
+
function parseCheckpointPlanSummaryUpdate(value) {
|
|
3710
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3711
|
+
return;
|
|
3712
|
+
}
|
|
3713
|
+
const record = value;
|
|
3714
|
+
const update = record.update === true;
|
|
3715
|
+
const summary = checkpointPlanString(record, "value") ?? checkpointPlanString(record, "summary") ?? checkpointPlanString(record, "currentSummary");
|
|
3716
|
+
const reason = checkpointPlanString(record, "reason");
|
|
3717
|
+
return {
|
|
3718
|
+
update,
|
|
3719
|
+
...summary ? { value: summary } : {},
|
|
3720
|
+
...reason ? { reason } : {}
|
|
3721
|
+
};
|
|
3722
|
+
}
|
|
3723
|
+
function parseCheckpointPlanSession(value) {
|
|
3724
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3725
|
+
return;
|
|
3726
|
+
}
|
|
3727
|
+
const record = value;
|
|
3728
|
+
const action = checkpointPlanString(record, "action", 40) ?? "keep";
|
|
3729
|
+
if (!checkpointSessionActions.includes(action)) {
|
|
3730
|
+
throw checkpointValidationError("Checkpoint plan session.action is invalid.", { action });
|
|
3731
|
+
}
|
|
3732
|
+
const summary = checkpointPlanString(record, "summary");
|
|
3733
|
+
const reason = checkpointPlanString(record, "reason");
|
|
3734
|
+
return {
|
|
3735
|
+
action,
|
|
3736
|
+
...summary ? { summary } : {},
|
|
3737
|
+
...reason ? { reason } : {}
|
|
3738
|
+
};
|
|
3739
|
+
}
|
|
3740
|
+
function parseCheckpointPlanFiles(value) {
|
|
3741
|
+
if (!Array.isArray(value)) {
|
|
3742
|
+
return [];
|
|
3743
|
+
}
|
|
3744
|
+
return value.map((item) => {
|
|
3745
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
3746
|
+
throw checkpointValidationError("Checkpoint plan files must be objects.");
|
|
3747
|
+
}
|
|
3748
|
+
const record = item;
|
|
3749
|
+
const path = checkpointPlanString(record, "path", 500);
|
|
3750
|
+
if (!path || isAbsolute(path) || path.split("/").includes("..")) {
|
|
3751
|
+
throw checkpointValidationError("Checkpoint plan file path must be a safe relative path.", { path });
|
|
3752
|
+
}
|
|
3753
|
+
return {
|
|
3754
|
+
path,
|
|
3755
|
+
kind: parseSessionFileChangeKind(checkpointPlanString(record, "kind", 40) ?? checkpointPlanString(record, "changeKind", 40))
|
|
3756
|
+
};
|
|
3757
|
+
});
|
|
3758
|
+
}
|
|
3759
|
+
function routeRequiresHighConfidence(route, session) {
|
|
3760
|
+
return highAutonomyRouteActions.has(route.action) || session?.action === "complete_ticket";
|
|
3761
|
+
}
|
|
3762
|
+
function forceNeedsHuman(plan, reason) {
|
|
3763
|
+
return {
|
|
3764
|
+
...plan,
|
|
3765
|
+
route: {
|
|
3766
|
+
...plan.route,
|
|
3767
|
+
action: "needs_human",
|
|
3768
|
+
reason
|
|
3769
|
+
},
|
|
3770
|
+
validationWarnings: [...plan.validationWarnings, reason]
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
function normalizeCheckpointPlan(plan) {
|
|
3774
|
+
if (routeRequiresHighConfidence(plan.route, plan.session) && plan.route.confidence !== "high") {
|
|
3775
|
+
return forceNeedsHuman(plan, "Lifecycle mutation requires high confidence.");
|
|
3776
|
+
}
|
|
3777
|
+
if (plan.route.action === "noop") {
|
|
3778
|
+
return {
|
|
3779
|
+
...plan,
|
|
3780
|
+
entries: []
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
return plan;
|
|
3784
|
+
}
|
|
3785
|
+
function parseCheckpointPlanRecord(record, rawText, fallbackKind) {
|
|
3786
|
+
if (!("route" in record)) {
|
|
3787
|
+
const legacy = parseCheckpointRecord(record, rawText, fallbackKind);
|
|
3788
|
+
const reason = legacy.currentSummary?.reason ?? legacy.summary;
|
|
3789
|
+
const route2 = {
|
|
3790
|
+
action: legacy.entries.length ? "current" : "noop",
|
|
3791
|
+
confidence: "high",
|
|
3792
|
+
...reason ? { reason } : {}
|
|
3793
|
+
};
|
|
3794
|
+
return normalizeCheckpointPlan({
|
|
3795
|
+
...legacy.summary ? { summary: legacy.summary } : {},
|
|
3796
|
+
route: route2,
|
|
3797
|
+
entries: legacy.entries,
|
|
3798
|
+
...legacy.currentSummary ? { summaryUpdate: legacy.currentSummary } : {},
|
|
3799
|
+
files: [],
|
|
3800
|
+
legacy: true,
|
|
3801
|
+
validationWarnings: []
|
|
3802
|
+
});
|
|
3803
|
+
}
|
|
3804
|
+
const route = parseCheckpointRoute(record.route);
|
|
3805
|
+
const entries = Array.isArray(record.entries) ? record.entries.map(parseCheckpointPlanEntry) : [];
|
|
3806
|
+
const summary = checkpointPlanString(record, "summary");
|
|
3807
|
+
const summaryUpdate = parseCheckpointPlanSummaryUpdate(record.summaryUpdate ?? record.currentSummary);
|
|
3808
|
+
const session = parseCheckpointPlanSession(record.session);
|
|
3809
|
+
return normalizeCheckpointPlan({
|
|
3810
|
+
...summary ? { summary } : {},
|
|
3811
|
+
route,
|
|
3812
|
+
entries,
|
|
3813
|
+
...summaryUpdate ? { summaryUpdate } : {},
|
|
3814
|
+
...session ? { session } : {},
|
|
3815
|
+
files: parseCheckpointPlanFiles(record.files),
|
|
3816
|
+
legacy: false,
|
|
3817
|
+
validationWarnings: []
|
|
3818
|
+
});
|
|
3819
|
+
}
|
|
3820
|
+
function parseCheckpointPlanJson(raw, fallbackKind) {
|
|
3821
|
+
const trimmed = raw.trim();
|
|
3822
|
+
try {
|
|
3823
|
+
const parsed = JSON.parse(trimmed);
|
|
3824
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3825
|
+
return parseCheckpointPlanRecord(parsed, trimmed, fallbackKind);
|
|
3826
|
+
}
|
|
3827
|
+
} catch (error) {
|
|
3828
|
+
if (error instanceof CliError) {
|
|
3829
|
+
throw error;
|
|
3830
|
+
}
|
|
3831
|
+
const start = trimmed.indexOf("{");
|
|
3832
|
+
const end = trimmed.lastIndexOf("}");
|
|
3833
|
+
if (start >= 0 && end > start) {
|
|
3834
|
+
try {
|
|
3835
|
+
const parsed = JSON.parse(trimmed.slice(start, end + 1));
|
|
3836
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3837
|
+
return parseCheckpointPlanRecord(parsed, trimmed, fallbackKind);
|
|
3838
|
+
}
|
|
3839
|
+
} catch (innerError) {
|
|
3840
|
+
if (innerError instanceof CliError) {
|
|
3841
|
+
throw innerError;
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
const legacy = parseCheckpointJson(trimmed, fallbackKind);
|
|
3847
|
+
const reason = legacy.currentSummary?.reason ?? legacy.summary;
|
|
3848
|
+
return normalizeCheckpointPlan({
|
|
3849
|
+
...legacy.summary ? { summary: legacy.summary } : {},
|
|
3850
|
+
route: {
|
|
3851
|
+
action: legacy.entries.length ? "current" : "noop",
|
|
3852
|
+
confidence: "high",
|
|
3853
|
+
...reason ? { reason } : {}
|
|
3854
|
+
},
|
|
3855
|
+
entries: legacy.entries,
|
|
3856
|
+
...legacy.currentSummary ? { summaryUpdate: legacy.currentSummary } : {},
|
|
3857
|
+
files: [],
|
|
3858
|
+
legacy: true,
|
|
3859
|
+
validationWarnings: []
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
function checkpointRouteRequiresIntake(action) {
|
|
3863
|
+
return action === "intake_create" || action === "intake_attach" || action === "switch_existing";
|
|
3864
|
+
}
|
|
3865
|
+
function checkpointPlanCompletionAllowed(event, summary) {
|
|
3866
|
+
const text = [
|
|
3867
|
+
summary,
|
|
3868
|
+
event.lastAssistantMessage ?? "",
|
|
3869
|
+
...(event.recentTurns ?? []).flatMap((turn) => [turn.user ?? "", turn.assistant ?? ""])
|
|
3870
|
+
].join(`
|
|
3871
|
+
`).toLowerCase();
|
|
3872
|
+
return /\b(done|complete|completed|finish|finished|close|closed|ship|shipped|push|pushed|pr|pull request|merged|clean branch|verified)\b/.test(text);
|
|
3873
|
+
}
|
|
3874
|
+
function checkpointPlanTitle(plan) {
|
|
3875
|
+
const intent = plan.entries.find((entry) => entry.kind === "intent");
|
|
3876
|
+
return truncateText(intent?.summary ?? plan.summary ?? plan.route.request ?? "Checkpoint routed work", 120);
|
|
3877
|
+
}
|
|
3878
|
+
function checkpointPlanDescription(plan) {
|
|
3879
|
+
return truncateText(plan.route.request ?? plan.summary ?? plan.route.reason ?? checkpointPlanTitle(plan), 1000);
|
|
3880
|
+
}
|
|
3881
|
+
function checkpointHasConflictingIntakeResult(intake, plan) {
|
|
3882
|
+
if (!intake || typeof intake !== "object" || Array.isArray(intake)) {
|
|
3883
|
+
return false;
|
|
3884
|
+
}
|
|
3885
|
+
const record = intake;
|
|
3886
|
+
const classification = typeof record.classification === "string" ? record.classification : undefined;
|
|
3887
|
+
const candidates = Array.isArray(record.candidates) ? record.candidates.filter((candidate) => candidate && typeof candidate === "object") : [];
|
|
3888
|
+
if (plan.route.action === "intake_create" && classification && classification !== "new") {
|
|
3889
|
+
return true;
|
|
3890
|
+
}
|
|
3891
|
+
if ((plan.route.action === "intake_attach" || plan.route.action === "switch_existing") && plan.route.targetTicketId && candidates.length > 0) {
|
|
3892
|
+
return !candidates.some((candidate) => {
|
|
3893
|
+
const candidateRecord = candidate;
|
|
3894
|
+
return candidateRecord.id === plan.route.targetTicketId || candidateRecord.ticketId === plan.route.targetTicketId;
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3897
|
+
return false;
|
|
3898
|
+
}
|
|
3899
|
+
function checkpointPlanNeedsHuman(plan, reason) {
|
|
3900
|
+
return forceNeedsHuman(plan, reason);
|
|
3901
|
+
}
|
|
3902
|
+
function checkpointLifecycleOperation(type, success, details = {}) {
|
|
3903
|
+
return {
|
|
3904
|
+
type,
|
|
3905
|
+
success,
|
|
3906
|
+
...details
|
|
3907
|
+
};
|
|
3908
|
+
}
|
|
3909
|
+
function checkpointRecentTurns(event) {
|
|
3910
|
+
return event.recentTurns?.length ? event.recentTurns.slice(-3) : [];
|
|
3911
|
+
}
|
|
3912
|
+
function formatCheckpointRecentTurns(turns) {
|
|
3913
|
+
return turns.map((turn, index) => [
|
|
3914
|
+
`Turn ${index + 1}${turn.occurredAt ? ` (${turn.occurredAt})` : ""}:`,
|
|
3915
|
+
turn.user ? `User:
|
|
3916
|
+
${truncateText(turn.user, 3000)}` : "User: unavailable",
|
|
3917
|
+
turn.assistant ? `Assistant:
|
|
3918
|
+
${truncateText(turn.assistant, 3000)}` : "Assistant: unavailable"
|
|
3919
|
+
].join(`
|
|
3920
|
+
`)).join(`
|
|
3921
|
+
|
|
3922
|
+
`);
|
|
3923
|
+
}
|
|
3924
|
+
function checkpointCapturedContextTokenCounts(event) {
|
|
3925
|
+
const turns = checkpointRecentTurns(event);
|
|
3926
|
+
if (!turns.length) {
|
|
3927
|
+
const fallbackText = event.lastAssistantMessage ? truncateText(event.lastAssistantMessage, 3000) : "";
|
|
3928
|
+
const fallbackTokens = fallbackText ? estimateTokenCount(fallbackText) : 0;
|
|
3929
|
+
return {
|
|
3930
|
+
mode: fallbackText ? "last_assistant_message" : "unavailable",
|
|
3931
|
+
turnCount: 0,
|
|
3932
|
+
userPromptTokens: 0,
|
|
3933
|
+
assistantResponseTokens: fallbackTokens,
|
|
3934
|
+
totalTokens: fallbackTokens
|
|
3935
|
+
};
|
|
3936
|
+
}
|
|
3937
|
+
const userPromptTokens = turns.reduce((total, turn) => total + (turn.user ? estimateTokenCount(truncateText(turn.user, 3000)) : 0), 0);
|
|
3938
|
+
const assistantResponseTokens = turns.reduce((total, turn) => total + (turn.assistant ? estimateTokenCount(truncateText(turn.assistant, 3000)) : 0), 0);
|
|
3939
|
+
return {
|
|
3940
|
+
mode: "recent_turns",
|
|
3941
|
+
turnCount: turns.length,
|
|
3942
|
+
userPromptTokens,
|
|
3943
|
+
assistantResponseTokens,
|
|
3944
|
+
totalTokens: estimateTokenCount(formatCheckpointRecentTurns(turns))
|
|
3945
|
+
};
|
|
3946
|
+
}
|
|
3947
|
+
function buildCheckpointTokenAccounting(event, prompt, tokenUsage) {
|
|
3948
|
+
const capturedContext = checkpointCapturedContextTokenCounts(event);
|
|
3949
|
+
const explicitCheckpointPromptTokens = estimateTokenCount(prompt);
|
|
3950
|
+
const explicitCadenceOverheadTokens = Math.max(0, explicitCheckpointPromptTokens - capturedContext.totalTokens);
|
|
3951
|
+
const reported = tokenUsageTotal(tokenUsage);
|
|
3952
|
+
const reportedInputTokens = reported?.inputTokens;
|
|
3953
|
+
const reportedCadenceOverheadTokens = typeof reportedInputTokens === "number" ? Math.max(0, reportedInputTokens - capturedContext.totalTokens) : undefined;
|
|
3954
|
+
return {
|
|
3955
|
+
source: "codex_token_count_plus_local_estimates",
|
|
3956
|
+
estimator: "cadence-simple-estimate-v1",
|
|
3957
|
+
capturedContext,
|
|
3958
|
+
explicitCheckpointPromptTokens,
|
|
3959
|
+
explicitCadenceOverheadTokens,
|
|
3960
|
+
explicitCadenceOverheadRatio: roundedRatio(explicitCadenceOverheadTokens, Math.max(1, capturedContext.totalTokens)),
|
|
3961
|
+
...reported ? {
|
|
3962
|
+
reported: {
|
|
3963
|
+
...reported.inputTokens !== undefined ? { inputTokens: reported.inputTokens } : {},
|
|
3964
|
+
...reported.cachedInputTokens !== undefined ? { cachedInputTokens: reported.cachedInputTokens } : {},
|
|
3965
|
+
...reported.outputTokens !== undefined ? { outputTokens: reported.outputTokens } : {},
|
|
3966
|
+
...reported.reasoningOutputTokens !== undefined ? { reasoningOutputTokens: reported.reasoningOutputTokens } : {},
|
|
3967
|
+
...reported.totalTokens !== undefined ? { totalTokens: reported.totalTokens } : {},
|
|
3968
|
+
...tokenUsage?.modelContextWindow !== undefined ? { modelContextWindow: tokenUsage.modelContextWindow } : {}
|
|
3969
|
+
}
|
|
3970
|
+
} : {},
|
|
3971
|
+
...reportedCadenceOverheadTokens !== undefined ? {
|
|
3972
|
+
reportedCadenceOverheadTokens,
|
|
3973
|
+
reportedCadenceOverheadRatio: roundedRatio(reportedCadenceOverheadTokens, Math.max(1, capturedContext.totalTokens))
|
|
3974
|
+
} : {}
|
|
3975
|
+
};
|
|
3976
|
+
}
|
|
3977
|
+
function buildCheckpointPrompt(input) {
|
|
3978
|
+
const recentTurns = checkpointRecentTurns(input.event);
|
|
3979
|
+
const recentTurnsText = recentTurns.length ? formatCheckpointRecentTurns(recentTurns) : "";
|
|
3980
|
+
return [
|
|
3981
|
+
"You are generating a compact Cadence dogfood operation plan for a hook-spawned checkpoint worker.",
|
|
3982
|
+
"The model judges; the Cadence CLI validates and executes. Return JSON only. Do not call tools to mutate Cadence yourself.",
|
|
3983
|
+
"Preserve durable ticket purpose. Identify user intent, changed intent, corrections, decisions, rationale, implementation actions, verification, blockers, and useful notes.",
|
|
3984
|
+
"Do not include raw diffs, raw transcripts, terminal logs, tool streams, secrets, file contents, or model reasoning in server-bound fields.",
|
|
3985
|
+
'Return this sparse JSON shape: {"summary":"short checkpoint summary","route":{"action":"current|noop|intake_create|intake_attach|switch_existing|needs_human","confidence":"low|medium|high","reason":"short reason","request":"natural language work request when intake is needed","targetTicketId":"optional existing ticket id"},"entries":[{"kind":"intent|decision|rationale|action|verification|blocker|correction|note","summary":"short entry summary","body":"safe Cadence work-log body","under":"optional: last, ticket-last, session-last, last-decision, last-correction, last-action, or entry UUID"}],"summaryUpdate":{"update":false,"value":null,"reason":"short reason"},"session":{"action":"keep|handoff|end|complete_ticket","summary":"optional handoff or completion summary","reason":"short reason"},"files":[{"path":"relative/path.ts","kind":"added|modified|deleted|renamed|unknown"}]}',
|
|
3986
|
+
"Keep output sparse: omit summaryUpdate, session, and files unless needed. Use route.action noop with entries [] for filler, acknowledgements, or other turns with nothing durable to record.",
|
|
3987
|
+
"Use route.action current for the same ticket. Use intake_create for clearly unrelated new work. Use intake_attach or switch_existing only when a specific existing ticket is clearly the right target. Use needs_human when routing is ambiguous.",
|
|
3988
|
+
"Set route.confidence high only when the recent user/assistant context makes the route and lifecycle action clear. Lifecycle mutations require high confidence.",
|
|
3989
|
+
"Use note only as a last-resort context kind. Prefer intent, decision, rationale, action, verification, correction, or blocker when those fit.",
|
|
3990
|
+
"Each entry body should be concise narrative, not a transcript. Keep each entry under 800 characters and avoid duplicating the same fact across entries.",
|
|
3991
|
+
"Set summaryUpdate.update true only when the durable current work summary is missing or misleading; otherwise omit summaryUpdate or leave update false.",
|
|
3992
|
+
"Use session.action complete_ticket only when the context indicates completion, merge, push/PR finalization, or explicit user completion intent. Use handoff/end only when the current session should close.",
|
|
3993
|
+
"",
|
|
3994
|
+
`Ticket: ${input.ticketId}`,
|
|
3995
|
+
input.sessionId ? `Session: ${input.sessionId}` : "",
|
|
3996
|
+
input.changesetId ? `ChangeSet: ${input.changesetId}` : "",
|
|
3997
|
+
`Agent event: ${input.event.source}/${input.event.event}`,
|
|
3998
|
+
input.previousCheckpointSummary ? `Previous checkpoint summary: ${input.previousCheckpointSummary}` : "",
|
|
3999
|
+
recentTurnsText ? `Recent user/assistant turns (most recent 3, local checkpoint context only):
|
|
4000
|
+
${recentTurnsText}` : input.event.lastAssistantMessage ? `Last assistant message:
|
|
4001
|
+
${truncateText(input.event.lastAssistantMessage, 3000)}` : "Recent turns: unavailable",
|
|
2989
4002
|
input.gitStatus ? `Git status --short:
|
|
2990
4003
|
${truncateText(input.gitStatus, 2000)}` : "Git status --short: clean or unavailable",
|
|
2991
4004
|
input.gitDiffStat ? `Git diff --stat origin/dev...:
|
|
@@ -3018,6 +4031,22 @@ function codexHookEntry(command) {
|
|
|
3018
4031
|
]
|
|
3019
4032
|
};
|
|
3020
4033
|
}
|
|
4034
|
+
function codexHookCommands(entry) {
|
|
4035
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
4036
|
+
return [];
|
|
4037
|
+
}
|
|
4038
|
+
const record = entry;
|
|
4039
|
+
const directCommand = typeof record.command === "string" ? [record.command] : [];
|
|
4040
|
+
const handlers = Array.isArray(record.hooks) ? record.hooks : [];
|
|
4041
|
+
const handlerCommands = handlers.flatMap((handler) => {
|
|
4042
|
+
if (!handler || typeof handler !== "object" || Array.isArray(handler)) {
|
|
4043
|
+
return [];
|
|
4044
|
+
}
|
|
4045
|
+
const command = handler.command;
|
|
4046
|
+
return typeof command === "string" ? [command] : [];
|
|
4047
|
+
});
|
|
4048
|
+
return [...directCommand, ...handlerCommands];
|
|
4049
|
+
}
|
|
3021
4050
|
async function readJsonObjectFile(filePath) {
|
|
3022
4051
|
const file = Bun.file(filePath);
|
|
3023
4052
|
if (!await file.exists()) {
|
|
@@ -3031,8 +4060,9 @@ async function installCodexHookFile(filePath, command) {
|
|
|
3031
4060
|
const hooks = hooksValue && typeof hooksValue === "object" && !Array.isArray(hooksValue) ? hooksValue : {};
|
|
3032
4061
|
const stopValue = hooks.Stop;
|
|
3033
4062
|
const stopHooks = Array.isArray(stopValue) ? stopValue : [];
|
|
3034
|
-
const alreadyInstalled = stopHooks.some((entry) =>
|
|
3035
|
-
const
|
|
4063
|
+
const alreadyInstalled = stopHooks.some((entry) => codexHookCommands(entry).includes(command));
|
|
4064
|
+
const cadenceHookIndexes = stopHooks.map((entry, index) => codexHookCommands(entry).some((entryCommand) => entryCommand.includes("agent-run ingest-stop --source codex --event stop")) ? index : -1).filter((index) => index >= 0);
|
|
4065
|
+
const nextStopHooks = alreadyInstalled ? stopHooks : cadenceHookIndexes.length ? stopHooks.map((entry, index) => index === cadenceHookIndexes[0] ? codexHookEntry(command) : entry).filter((_, index) => !cadenceHookIndexes.slice(1).includes(index)) : [...stopHooks, codexHookEntry(command)];
|
|
3036
4066
|
const nextConfig = {
|
|
3037
4067
|
...existing,
|
|
3038
4068
|
hooks: {
|
|
@@ -3046,7 +4076,8 @@ async function installCodexHookFile(filePath, command) {
|
|
|
3046
4076
|
return {
|
|
3047
4077
|
path: filePath,
|
|
3048
4078
|
installed: !alreadyInstalled,
|
|
3049
|
-
alreadyInstalled
|
|
4079
|
+
alreadyInstalled,
|
|
4080
|
+
updated: !alreadyInstalled && cadenceHookIndexes.length > 0
|
|
3050
4081
|
};
|
|
3051
4082
|
}
|
|
3052
4083
|
async function codexHookPaths(scope, options) {
|
|
@@ -3068,7 +4099,7 @@ async function codexHookInstalled(filePath, command) {
|
|
|
3068
4099
|
const existing = await readJsonObjectFile(filePath);
|
|
3069
4100
|
const hooks = existing.hooks;
|
|
3070
4101
|
const stopHooks = hooks && typeof hooks === "object" && !Array.isArray(hooks) && Array.isArray(hooks.Stop) ? hooks.Stop : [];
|
|
3071
|
-
return stopHooks.some((entry) =>
|
|
4102
|
+
return stopHooks.some((entry) => codexHookCommands(entry).includes(command));
|
|
3072
4103
|
}
|
|
3073
4104
|
async function runStatus(parsed, options) {
|
|
3074
4105
|
const config = await resolveCliConfig(parsed.flags, options);
|
|
@@ -3215,14 +4246,17 @@ async function runAgentRunCommand(parsed, options) {
|
|
|
3215
4246
|
switch (parsed.command.name) {
|
|
3216
4247
|
case "agent-run.ingest-stop":
|
|
3217
4248
|
return await runAgentRunIngestStop(parsed, options, config, meta);
|
|
4249
|
+
case "agent-run.checkpoint":
|
|
3218
4250
|
case "agent-run.closeout":
|
|
3219
|
-
return await
|
|
4251
|
+
return await runAgentRunCheckpoint(parsed, options, config, meta);
|
|
3220
4252
|
case "agent-run.sweep":
|
|
3221
|
-
return await runAgentRunSweep(parsed, options, meta);
|
|
4253
|
+
return await runAgentRunSweep(parsed, options, config, meta);
|
|
3222
4254
|
case "agent-run.doctor": {
|
|
3223
4255
|
const state = await readAgentLoopState(parsed, options);
|
|
4256
|
+
const settings = await readAgentLoopSettings(parsed, options);
|
|
3224
4257
|
const data = {
|
|
3225
4258
|
action: "doctor",
|
|
4259
|
+
settings,
|
|
3226
4260
|
state,
|
|
3227
4261
|
sessionCount: Object.keys(state.sessions).length,
|
|
3228
4262
|
pendingSessions: Object.entries(state.sessions).filter(([, session]) => session.stopCount > 0).map(([agentSessionKeyValue, session]) => ({
|
|
@@ -3297,8 +4331,15 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
3297
4331
|
const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
|
|
3298
4332
|
const existingSession = state.sessions[normalized.agentSessionKey];
|
|
3299
4333
|
const now = new Date().toISOString();
|
|
3300
|
-
const threshold = forcedThreshold ?? existingSession?.threshold ??
|
|
3301
|
-
const
|
|
4334
|
+
const threshold = forcedThreshold ?? existingSession?.threshold ?? defaultCheckpointThresholdValue();
|
|
4335
|
+
const duplicateTurn = Boolean(normalized.turnId && existingSession?.lastObservedTurnId === normalized.turnId);
|
|
4336
|
+
const nextCount = (existingSession?.stopCount ?? 0) + (duplicateTurn ? 0 : 1);
|
|
4337
|
+
const recentTurns = mergeRecentAgentTurns(existingSession?.recentTurns, normalized.recentTurns);
|
|
4338
|
+
let cadenceContext = existingSession?.cadenceContext;
|
|
4339
|
+
try {
|
|
4340
|
+
const client = await createClient(config, options);
|
|
4341
|
+
cadenceContext = await readCurrentCadenceContext(client, projectId) ?? cadenceContext;
|
|
4342
|
+
} catch {}
|
|
3302
4343
|
const observedSession = {
|
|
3303
4344
|
...clearAgentSessionReason(existingSession ?? {
|
|
3304
4345
|
source: normalized.source,
|
|
@@ -3310,8 +4351,11 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
3310
4351
|
threshold,
|
|
3311
4352
|
firstObservedAt: existingSession?.firstObservedAt ?? now,
|
|
3312
4353
|
lastObservedAt: now,
|
|
4354
|
+
...normalized.turnId ? { lastObservedTurnId: normalized.turnId } : {},
|
|
3313
4355
|
lastAction: "counted",
|
|
3314
|
-
...
|
|
4356
|
+
...cadenceContext ? { cadenceContext } : {},
|
|
4357
|
+
...normalized.lastAssistantMessage ? { lastAssistantMessage: normalized.lastAssistantMessage } : {},
|
|
4358
|
+
...recentTurns ? { recentTurns } : {}
|
|
3315
4359
|
};
|
|
3316
4360
|
const countedState = {
|
|
3317
4361
|
...state,
|
|
@@ -3323,7 +4367,8 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
3323
4367
|
if (nextCount < threshold) {
|
|
3324
4368
|
await writeAgentLoopState(parsed, options, countedState);
|
|
3325
4369
|
const data2 = {
|
|
3326
|
-
action: "counted",
|
|
4370
|
+
action: duplicateTurn ? "updated" : "counted",
|
|
4371
|
+
...duplicateTurn ? { reason: "duplicate_turn" } : {},
|
|
3327
4372
|
agentSessionKey: normalized.agentSessionKey,
|
|
3328
4373
|
stopCount: nextCount,
|
|
3329
4374
|
threshold
|
|
@@ -3370,7 +4415,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
3370
4415
|
[normalized.agentSessionKey]: {
|
|
3371
4416
|
...observedSession,
|
|
3372
4417
|
stopCount: 0,
|
|
3373
|
-
threshold:
|
|
4418
|
+
threshold: defaultCheckpointThresholdValue(),
|
|
3374
4419
|
lastAction: "skipped",
|
|
3375
4420
|
lastReason: "unchanged"
|
|
3376
4421
|
}
|
|
@@ -3392,7 +4437,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
3392
4437
|
const lockPath = agentLoopLockPath(parsed, options, normalized.agentSessionKey);
|
|
3393
4438
|
const workerArgs = [
|
|
3394
4439
|
"agent-run",
|
|
3395
|
-
"
|
|
4440
|
+
"checkpoint",
|
|
3396
4441
|
"--agent-session-key",
|
|
3397
4442
|
normalized.agentSessionKey,
|
|
3398
4443
|
"--reason",
|
|
@@ -3458,7 +4503,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
3458
4503
|
};
|
|
3459
4504
|
}
|
|
3460
4505
|
try {
|
|
3461
|
-
await
|
|
4506
|
+
await spawnAgentRunCheckpoint(workerArgs, options);
|
|
3462
4507
|
} catch (error) {
|
|
3463
4508
|
await releaseAgentLoopLock(lock.lockPath);
|
|
3464
4509
|
throw error;
|
|
@@ -3469,8 +4514,9 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
3469
4514
|
...state.sessions,
|
|
3470
4515
|
[normalized.agentSessionKey]: {
|
|
3471
4516
|
...observedSession,
|
|
4517
|
+
...cadenceContext ? { cadenceContext } : {},
|
|
3472
4518
|
stopCount: 0,
|
|
3473
|
-
threshold:
|
|
4519
|
+
threshold: defaultCheckpointThresholdValue(),
|
|
3474
4520
|
lastAction: "spawned",
|
|
3475
4521
|
lastEventFile: eventFile,
|
|
3476
4522
|
lastCheckpointAt: new Date().toISOString(),
|
|
@@ -3491,7 +4537,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
3491
4537
|
exitCode: 0
|
|
3492
4538
|
};
|
|
3493
4539
|
}
|
|
3494
|
-
async function
|
|
4540
|
+
async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
3495
4541
|
const projectId = requireProjectId(config);
|
|
3496
4542
|
const client = await createClient(config, options);
|
|
3497
4543
|
const agentSessionKeyValue = requireOption(parsed, "agent-session-key");
|
|
@@ -3502,16 +4548,11 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
|
|
|
3502
4548
|
agentSessionKey: agentSessionKeyValue
|
|
3503
4549
|
});
|
|
3504
4550
|
}
|
|
3505
|
-
const
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
});
|
|
3511
|
-
const currentSession = activeSessionFromCurrent(current);
|
|
3512
|
-
const ticketId = parsed.options.ticket ?? (currentSession && typeof currentSession.ticketId === "string" ? currentSession.ticketId : undefined);
|
|
3513
|
-
const sessionId = parsed.options.session ?? (currentSession && typeof currentSession.id === "string" ? currentSession.id : undefined);
|
|
3514
|
-
const changesetId = parsed.options.changeset ?? (currentSession && typeof currentSession.changesetId === "string" ? currentSession.changesetId : undefined);
|
|
4551
|
+
const savedContext = sessionState.cadenceContext;
|
|
4552
|
+
const currentContext = parsed.options.ticket || savedContext?.ticketId ? undefined : await readCurrentCadenceContext(client, projectId);
|
|
4553
|
+
const ticketId = parsed.options.ticket ?? savedContext?.ticketId ?? currentContext?.ticketId;
|
|
4554
|
+
const sessionId = parsed.options.session ?? savedContext?.sessionId ?? currentContext?.sessionId;
|
|
4555
|
+
const changesetId = parsed.options.changeset ?? savedContext?.changesetId ?? currentContext?.changesetId;
|
|
3515
4556
|
if (!ticketId) {
|
|
3516
4557
|
await writeAgentLoopState(parsed, options, {
|
|
3517
4558
|
...state,
|
|
@@ -3542,6 +4583,7 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
|
|
|
3542
4583
|
const updateSummary = parseBooleanOption(parsed.options["update-summary"], false);
|
|
3543
4584
|
const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
|
|
3544
4585
|
const lockPath = parsed.options.lock;
|
|
4586
|
+
const checkpointSettings = await resolveCheckpointSettings(parsed, options);
|
|
3545
4587
|
const gitStatus = gitOutput(["status", "--short"], options);
|
|
3546
4588
|
const gitDiffStat = gitOutput(["diff", "--stat", "origin/dev..."], options);
|
|
3547
4589
|
const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
|
|
@@ -3555,10 +4597,29 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
|
|
|
3555
4597
|
changedFiles,
|
|
3556
4598
|
...sessionState.previousCheckpointSummary ? { previousCheckpointSummary: sessionState.previousCheckpointSummary } : {}
|
|
3557
4599
|
});
|
|
4600
|
+
const promptTokenAccounting = buildCheckpointTokenAccounting(event, prompt, undefined);
|
|
3558
4601
|
if (dryRun) {
|
|
4602
|
+
const auditFile = await writeAgentCheckpointAuditFile(parsed, options, {
|
|
4603
|
+
version: 1,
|
|
4604
|
+
action: "would_checkpoint",
|
|
4605
|
+
createdAt: new Date().toISOString(),
|
|
4606
|
+
localOnly: true,
|
|
4607
|
+
localOnlyReason: "Raw hook context and checkpoint prompts stay on this machine and are not uploaded to Cadence.",
|
|
4608
|
+
agentSessionKey: agentSessionKeyValue,
|
|
4609
|
+
ticketId,
|
|
4610
|
+
...sessionId ? { sessionId } : {},
|
|
4611
|
+
...changesetId ? { changesetId } : {},
|
|
4612
|
+
...eventFile ? { eventFile } : {},
|
|
4613
|
+
event,
|
|
4614
|
+
prompt,
|
|
4615
|
+
checkpointSettings,
|
|
4616
|
+
tokenAccounting: promptTokenAccounting,
|
|
4617
|
+
cadenceWrites: []
|
|
4618
|
+
});
|
|
3559
4619
|
const data = {
|
|
3560
|
-
action: "
|
|
4620
|
+
action: "would_checkpoint",
|
|
3561
4621
|
prompt,
|
|
4622
|
+
auditFile,
|
|
3562
4623
|
ticketId,
|
|
3563
4624
|
agentSessionKey: agentSessionKeyValue,
|
|
3564
4625
|
...sessionId ? { sessionId } : {},
|
|
@@ -3573,84 +4634,387 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
|
|
|
3573
4634
|
}
|
|
3574
4635
|
try {
|
|
3575
4636
|
const codexCommand = parsed.options["codex-command"] ?? "codex";
|
|
3576
|
-
const
|
|
3577
|
-
|
|
4637
|
+
const codexStartedAtMs = Date.now();
|
|
4638
|
+
const codexCwd = options.cwd ?? process.cwd();
|
|
4639
|
+
const codexArgs = [
|
|
4640
|
+
"exec",
|
|
4641
|
+
...checkpointSettings.model ? ["-m", checkpointSettings.model] : [],
|
|
4642
|
+
"--disable",
|
|
4643
|
+
"hooks",
|
|
4644
|
+
"--sandbox",
|
|
4645
|
+
"read-only",
|
|
4646
|
+
"-C",
|
|
4647
|
+
codexCwd,
|
|
4648
|
+
prompt
|
|
4649
|
+
];
|
|
4650
|
+
const codex = runLocalCommand(codexCommand, codexArgs, options, {
|
|
4651
|
+
cwd: codexCwd,
|
|
3578
4652
|
env: {
|
|
3579
4653
|
[agentLoopSuppressEnv]: "1",
|
|
3580
4654
|
CADENCE_HOOK_SUPPRESS: "1"
|
|
3581
4655
|
},
|
|
3582
4656
|
timeoutMs: defaultCheckpointWorkerTimeoutMs
|
|
3583
4657
|
});
|
|
4658
|
+
const codexSessionTranscript = readCodexSessionTranscript(findCodexSessionFileCreatedAfter(options, codexStartedAtMs, codexCwd));
|
|
4659
|
+
const tokenAccounting = buildCheckpointTokenAccounting(event, prompt, codexSessionTranscript?.tokenUsage);
|
|
3584
4660
|
if (codex.status !== 0 || codex.error) {
|
|
3585
|
-
throw new CliError("
|
|
4661
|
+
throw new CliError("AGENT_RUN_CHECKPOINT_FAILED", "Codex checkpoint generation failed.", {
|
|
3586
4662
|
status: codex.status,
|
|
3587
4663
|
stderr: truncateText(codex.stderr, 2000),
|
|
3588
4664
|
error: codex.error?.message
|
|
3589
4665
|
});
|
|
3590
4666
|
}
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
4667
|
+
let checkpoint = parseCheckpointPlanJson(codex.stdout, logKind);
|
|
4668
|
+
let summary = checkpoint.summary ?? checkpoint.entries[0]?.summary ?? checkpoint.route.reason ?? "Agent run checkpoint";
|
|
4669
|
+
let targetTicketId = ticketId;
|
|
4670
|
+
let targetSessionId = sessionId;
|
|
4671
|
+
let targetChangesetId = changesetId;
|
|
4672
|
+
const cadenceWrites = [];
|
|
4673
|
+
const lifecycleOperations = [];
|
|
4674
|
+
let intakeResult;
|
|
4675
|
+
let selectedTicket;
|
|
4676
|
+
const modelAudit = {
|
|
4677
|
+
provider: checkpointSettings.provider,
|
|
4678
|
+
command: codexCommand,
|
|
4679
|
+
...checkpointSettings.model ? { model: checkpointSettings.model } : {},
|
|
4680
|
+
status: codex.status,
|
|
4681
|
+
stdout: codex.stdout.trim(),
|
|
4682
|
+
stderr: truncateText(codex.stderr.trim(), 2000),
|
|
4683
|
+
tokenAccounting,
|
|
4684
|
+
...codexSessionTranscript?.tokenUsage ? { tokenUsage: codexSessionTranscript.tokenUsage } : {},
|
|
4685
|
+
...codexSessionTranscript ? { sessionTranscript: codexSessionTranscript } : {}
|
|
4686
|
+
};
|
|
4687
|
+
const finishCheckpoint = async (action, reason) => {
|
|
4688
|
+
const checkedAt = new Date().toISOString();
|
|
4689
|
+
const auditFile = await writeAgentCheckpointAuditFile(parsed, options, {
|
|
4690
|
+
version: 1,
|
|
4691
|
+
action,
|
|
4692
|
+
createdAt: checkedAt,
|
|
4693
|
+
localOnly: true,
|
|
4694
|
+
localOnlyReason: action === "noop" || action === "needs_human" ? "Raw hook context, checkpoint prompts, and model output stay on this machine; no Cadence writes were needed." : "Raw hook context, checkpoint prompts, and model output stay on this machine; Cadence receives only the structured writes listed here.",
|
|
4695
|
+
agentSessionKey: agentSessionKeyValue,
|
|
4696
|
+
ticketId: targetTicketId,
|
|
4697
|
+
...targetSessionId ? { sessionId: targetSessionId } : {},
|
|
4698
|
+
...targetChangesetId ? { changesetId: targetChangesetId } : {},
|
|
4699
|
+
...eventFile ? { eventFile } : {},
|
|
4700
|
+
event,
|
|
4701
|
+
prompt,
|
|
4702
|
+
model: modelAudit,
|
|
4703
|
+
checkpoint,
|
|
4704
|
+
route: checkpoint.route,
|
|
4705
|
+
...intakeResult ? { intakeResult } : {},
|
|
4706
|
+
...selectedTicket ? { selectedTicket } : {},
|
|
4707
|
+
lifecycleOperations,
|
|
4708
|
+
cadenceWrites,
|
|
3603
4709
|
summary,
|
|
3604
|
-
...
|
|
3605
|
-
|
|
4710
|
+
...reason ? { reason } : {},
|
|
4711
|
+
entryCount: checkpoint.entries.length,
|
|
4712
|
+
needsHuman: action === "needs_human"
|
|
4713
|
+
});
|
|
4714
|
+
await writeAgentLoopState(parsed, options, {
|
|
4715
|
+
...state,
|
|
4716
|
+
sessions: {
|
|
4717
|
+
...state.sessions,
|
|
4718
|
+
[agentSessionKeyValue]: {
|
|
4719
|
+
...sessionState,
|
|
4720
|
+
stopCount: 0,
|
|
4721
|
+
threshold: defaultCheckpointThresholdValue(),
|
|
4722
|
+
previousCheckpointSummary: summary,
|
|
4723
|
+
lastAction: action,
|
|
4724
|
+
...reason ? { lastReason: reason } : {},
|
|
4725
|
+
...eventFile ? { lastEventFile: eventFile } : {},
|
|
4726
|
+
cadenceContext: {
|
|
4727
|
+
ticketId: targetTicketId,
|
|
4728
|
+
...targetSessionId ? { sessionId: targetSessionId } : {},
|
|
4729
|
+
...targetChangesetId ? { changesetId: targetChangesetId } : {},
|
|
4730
|
+
capturedAt: checkedAt
|
|
4731
|
+
},
|
|
4732
|
+
lastCheckpointAt: checkedAt,
|
|
4733
|
+
lastCheckpointAuditFile: auditFile
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
4736
|
+
});
|
|
4737
|
+
const data = {
|
|
4738
|
+
action,
|
|
4739
|
+
route: checkpoint.route,
|
|
4740
|
+
...reason ? { reason } : {},
|
|
4741
|
+
ticketId: targetTicketId,
|
|
4742
|
+
summary,
|
|
4743
|
+
entryCount: checkpoint.entries.length,
|
|
4744
|
+
auditFile,
|
|
4745
|
+
agentSessionKey: agentSessionKeyValue,
|
|
4746
|
+
...targetSessionId ? { sessionId: targetSessionId } : {},
|
|
4747
|
+
...targetChangesetId ? { changesetId: targetChangesetId } : {},
|
|
4748
|
+
...action === "needs_human" ? { needsHuman: true } : {}
|
|
4749
|
+
};
|
|
4750
|
+
return {
|
|
4751
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
|
|
4752
|
+
`,
|
|
4753
|
+
stderr: "",
|
|
4754
|
+
exitCode: 0
|
|
4755
|
+
};
|
|
4756
|
+
};
|
|
4757
|
+
if (checkpoint.session?.action === "complete_ticket" && !checkpointPlanCompletionAllowed(event, summary)) {
|
|
4758
|
+
checkpoint = checkpointPlanNeedsHuman(checkpoint, "Completion requires explicit recent completion, push, PR, merge, clean branch, or verification intent.");
|
|
4759
|
+
}
|
|
4760
|
+
if (checkpoint.route.action === "noop") {
|
|
4761
|
+
return await finishCheckpoint("noop", checkpoint.route.reason ?? checkpoint.summary ?? "Nothing durable to record.");
|
|
4762
|
+
}
|
|
4763
|
+
if (checkpoint.route.action === "needs_human") {
|
|
4764
|
+
return await finishCheckpoint("needs_human", checkpoint.route.reason ?? "Checkpoint routing needs human review.");
|
|
4765
|
+
}
|
|
4766
|
+
if (checkpointRouteRequiresIntake(checkpoint.route.action)) {
|
|
4767
|
+
const intakeRequest = checkpoint.route.request ?? checkpointPlanDescription(checkpoint);
|
|
4768
|
+
intakeResult = await client.intake.create({
|
|
4769
|
+
projectId,
|
|
4770
|
+
intake: {
|
|
4771
|
+
request: intakeRequest,
|
|
4772
|
+
...commandMetadata()
|
|
4773
|
+
}
|
|
4774
|
+
});
|
|
4775
|
+
lifecycleOperations.push(checkpointLifecycleOperation("intake.created", true, { request: intakeRequest, result: intakeResult }));
|
|
4776
|
+
if (checkpointHasConflictingIntakeResult(intakeResult, checkpoint)) {
|
|
4777
|
+
checkpoint = checkpointPlanNeedsHuman(checkpoint, "Intake returned conflicting duplicate, overlap, or completed-before candidates.");
|
|
4778
|
+
return await finishCheckpoint("needs_human", checkpoint.route.reason);
|
|
4779
|
+
}
|
|
4780
|
+
if (checkpoint.route.action === "intake_create") {
|
|
4781
|
+
selectedTicket = await client.tickets.create({
|
|
4782
|
+
projectId,
|
|
4783
|
+
ticket: {
|
|
4784
|
+
title: checkpointPlanTitle(checkpoint),
|
|
4785
|
+
description: checkpointPlanDescription(checkpoint),
|
|
4786
|
+
fromIntakeId: typeof intakeResult === "object" && intakeResult && "id" in intakeResult ? String(intakeResult.id) : undefined,
|
|
4787
|
+
...commandMetadata()
|
|
4788
|
+
}
|
|
4789
|
+
});
|
|
4790
|
+
lifecycleOperations.push(checkpointLifecycleOperation("ticket.created", true, { ticket: selectedTicket }));
|
|
4791
|
+
if (selectedTicket && typeof selectedTicket === "object" && "id" in selectedTicket) {
|
|
4792
|
+
targetTicketId = String(selectedTicket.id);
|
|
4793
|
+
}
|
|
4794
|
+
} else {
|
|
4795
|
+
if (!checkpoint.route.targetTicketId) {
|
|
4796
|
+
checkpoint = checkpointPlanNeedsHuman(checkpoint, "Checkpoint route requires a target ticket id.");
|
|
4797
|
+
return await finishCheckpoint("needs_human", checkpoint.route.reason);
|
|
4798
|
+
}
|
|
4799
|
+
targetTicketId = checkpoint.route.targetTicketId;
|
|
4800
|
+
selectedTicket = await client.tickets.get({ projectId, ticketId: targetTicketId });
|
|
4801
|
+
lifecycleOperations.push(checkpointLifecycleOperation("ticket.selected", true, { ticketId: targetTicketId, ticket: selectedTicket }));
|
|
4802
|
+
const selectedVersion = selectedTicket && typeof selectedTicket === "object" && typeof selectedTicket.projectionVersion === "number" ? selectedTicket.projectionVersion : undefined;
|
|
4803
|
+
const intakeId = intakeResult && typeof intakeResult === "object" && "id" in intakeResult ? String(intakeResult.id) : undefined;
|
|
4804
|
+
if (selectedVersion !== undefined && intakeId) {
|
|
4805
|
+
await client.tickets.attach({
|
|
4806
|
+
projectId,
|
|
4807
|
+
ticketId: targetTicketId,
|
|
4808
|
+
fromIntakeId: intakeId,
|
|
4809
|
+
ifVersion: selectedVersion
|
|
4810
|
+
});
|
|
4811
|
+
lifecycleOperations.push(checkpointLifecycleOperation("intake.attached", true, { ticketId: targetTicketId, intakeId }));
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
if (targetTicketId !== ticketId && targetSessionId) {
|
|
4815
|
+
await client.sessions.end({
|
|
4816
|
+
projectId,
|
|
4817
|
+
sessionId: targetSessionId,
|
|
4818
|
+
session: {
|
|
4819
|
+
summary: checkpoint.session?.summary ?? checkpoint.route.reason ?? summary,
|
|
4820
|
+
...commandMetadata()
|
|
4821
|
+
}
|
|
4822
|
+
});
|
|
4823
|
+
lifecycleOperations.push(checkpointLifecycleOperation("session.ended", true, { sessionId: targetSessionId, reason: "ticket_switch" }));
|
|
4824
|
+
targetSessionId = undefined;
|
|
4825
|
+
targetChangesetId = undefined;
|
|
4826
|
+
}
|
|
4827
|
+
const startedSession = await client.sessions.start({
|
|
4828
|
+
projectId,
|
|
4829
|
+
session: {
|
|
4830
|
+
ticketId: targetTicketId,
|
|
4831
|
+
...commandMetadata()
|
|
4832
|
+
}
|
|
4833
|
+
});
|
|
4834
|
+
lifecycleOperations.push(checkpointLifecycleOperation("session.started", true, { ticketId: targetTicketId, session: startedSession }));
|
|
4835
|
+
if (startedSession && typeof startedSession === "object" && "id" in startedSession) {
|
|
4836
|
+
targetSessionId = String(startedSession.id);
|
|
4837
|
+
}
|
|
4838
|
+
if (targetSessionId) {
|
|
4839
|
+
const lease = await client.sessions.leases.create({
|
|
4840
|
+
projectId,
|
|
4841
|
+
lease: {
|
|
4842
|
+
ticketId: targetTicketId,
|
|
4843
|
+
sessionId: targetSessionId,
|
|
4844
|
+
expiresAt: leaseExpiresAt(defaultLeaseTtlSeconds),
|
|
4845
|
+
replaceOwnActiveLease: true,
|
|
4846
|
+
...commandMetadata()
|
|
4847
|
+
}
|
|
4848
|
+
});
|
|
4849
|
+
lifecycleOperations.push(checkpointLifecycleOperation("lease.claimed", true, { ticketId: targetTicketId, sessionId: targetSessionId, lease }));
|
|
4850
|
+
}
|
|
4851
|
+
if (targetSessionId) {
|
|
4852
|
+
try {
|
|
4853
|
+
const branchName = await resolveCurrentBranch(options);
|
|
4854
|
+
const changeset = await client.changesets.create({
|
|
4855
|
+
projectId,
|
|
4856
|
+
changeset: {
|
|
4857
|
+
ticketId: targetTicketId,
|
|
4858
|
+
branchName,
|
|
4859
|
+
baseBranch: "dev",
|
|
4860
|
+
sessionId: targetSessionId,
|
|
4861
|
+
...commandMetadata()
|
|
4862
|
+
}
|
|
4863
|
+
});
|
|
4864
|
+
lifecycleOperations.push(checkpointLifecycleOperation("changeset.linked", true, { ticketId: targetTicketId, branchName, changeset }));
|
|
4865
|
+
if (changeset && typeof changeset === "object" && "id" in changeset) {
|
|
4866
|
+
targetChangesetId = String(changeset.id);
|
|
4867
|
+
}
|
|
4868
|
+
} catch (error) {
|
|
4869
|
+
lifecycleOperations.push(checkpointLifecycleOperation("changeset.linked", false, { error: error instanceof Error ? error.message : String(error) }));
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
}
|
|
4873
|
+
const checkpointMetadata = checkpointWorkLogMetadata(checkpointSettings, tokenAccounting);
|
|
4874
|
+
for (const entry of checkpoint.entries) {
|
|
4875
|
+
const logEntry = {
|
|
4876
|
+
entryKind: entry.kind,
|
|
4877
|
+
body: entry.body,
|
|
4878
|
+
summary: entry.summary ?? checkpointEntrySummary(entry.body),
|
|
4879
|
+
...entry.parentSelector ? { parentSelector: entry.parentSelector } : {},
|
|
4880
|
+
...entry.parentEntryId ? { parentEntryId: entry.parentEntryId } : {},
|
|
4881
|
+
...targetSessionId ? { sessionId: targetSessionId } : {},
|
|
4882
|
+
...targetChangesetId ? { changesetId: targetChangesetId } : {},
|
|
4883
|
+
metadata: checkpointMetadata,
|
|
3606
4884
|
...commandMetadata()
|
|
4885
|
+
};
|
|
4886
|
+
try {
|
|
4887
|
+
await client.tickets.log({
|
|
4888
|
+
projectId,
|
|
4889
|
+
ticketId: targetTicketId,
|
|
4890
|
+
entry: logEntry
|
|
4891
|
+
});
|
|
4892
|
+
cadenceWrites.push({
|
|
4893
|
+
type: "ticket.work_log_appended",
|
|
4894
|
+
success: true,
|
|
4895
|
+
ticketId: targetTicketId,
|
|
4896
|
+
entry: logEntry
|
|
4897
|
+
});
|
|
4898
|
+
} catch (error) {
|
|
4899
|
+
if (!entry.parentSelector && !entry.parentEntryId) {
|
|
4900
|
+
cadenceWrites.push({
|
|
4901
|
+
type: "ticket.work_log_appended",
|
|
4902
|
+
success: false,
|
|
4903
|
+
ticketId: targetTicketId,
|
|
4904
|
+
entry: logEntry,
|
|
4905
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4906
|
+
});
|
|
4907
|
+
throw error;
|
|
4908
|
+
}
|
|
4909
|
+
const { parentSelector: _parentSelector, parentEntryId: _parentEntryId, ...entryWithoutParent } = logEntry;
|
|
4910
|
+
cadenceWrites.push({
|
|
4911
|
+
type: "ticket.work_log_appended",
|
|
4912
|
+
success: false,
|
|
4913
|
+
ticketId: targetTicketId,
|
|
4914
|
+
entry: logEntry,
|
|
4915
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4916
|
+
});
|
|
4917
|
+
await client.tickets.log({
|
|
4918
|
+
projectId,
|
|
4919
|
+
ticketId: targetTicketId,
|
|
4920
|
+
entry: entryWithoutParent
|
|
4921
|
+
});
|
|
4922
|
+
cadenceWrites.push({
|
|
4923
|
+
type: "ticket.work_log_appended",
|
|
4924
|
+
success: true,
|
|
4925
|
+
ticketId: targetTicketId,
|
|
4926
|
+
entry: entryWithoutParent,
|
|
4927
|
+
fallback: "removed unresolved parent reference"
|
|
4928
|
+
});
|
|
3607
4929
|
}
|
|
3608
|
-
}
|
|
3609
|
-
if (
|
|
3610
|
-
const
|
|
4930
|
+
}
|
|
4931
|
+
if (checkpoint.files.length && targetSessionId) {
|
|
4932
|
+
const filesByKind = new Map;
|
|
4933
|
+
for (const file of checkpoint.files) {
|
|
4934
|
+
filesByKind.set(file.kind, [...filesByKind.get(file.kind) ?? [], file.path]);
|
|
4935
|
+
}
|
|
4936
|
+
for (const [kind, paths] of filesByKind) {
|
|
4937
|
+
const filesPayload = {
|
|
4938
|
+
...targetChangesetId ? { changesetId: targetChangesetId } : {},
|
|
4939
|
+
files: paths.map((path) => ({
|
|
4940
|
+
path,
|
|
4941
|
+
changeKind: kind
|
|
4942
|
+
})),
|
|
4943
|
+
...commandMetadata()
|
|
4944
|
+
};
|
|
4945
|
+
await client.sessions.files({
|
|
4946
|
+
projectId,
|
|
4947
|
+
sessionId: targetSessionId,
|
|
4948
|
+
files: filesPayload
|
|
4949
|
+
});
|
|
4950
|
+
lifecycleOperations.push(checkpointLifecycleOperation("session.files_attached", true, { sessionId: targetSessionId, kind, files: paths }));
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
if ((updateSummary || checkpoint.summaryUpdate?.update) && checkpoint.summaryUpdate?.value) {
|
|
4954
|
+
const ticket = await client.tickets.get({ projectId, ticketId: targetTicketId });
|
|
3611
4955
|
if (typeof ticket.projectionVersion === "number") {
|
|
3612
4956
|
await client.tickets.update({
|
|
3613
4957
|
projectId,
|
|
3614
|
-
ticketId,
|
|
4958
|
+
ticketId: targetTicketId,
|
|
3615
4959
|
ifVersion: ticket.projectionVersion,
|
|
3616
4960
|
ticket: {
|
|
3617
|
-
currentSummary:
|
|
4961
|
+
currentSummary: checkpoint.summaryUpdate.value,
|
|
3618
4962
|
...commandMetadata()
|
|
3619
4963
|
}
|
|
3620
4964
|
});
|
|
4965
|
+
cadenceWrites.push({
|
|
4966
|
+
type: "ticket.updated",
|
|
4967
|
+
success: true,
|
|
4968
|
+
ticketId: targetTicketId,
|
|
4969
|
+
ifVersion: ticket.projectionVersion,
|
|
4970
|
+
ticket: {
|
|
4971
|
+
currentSummary: checkpoint.summaryUpdate.value
|
|
4972
|
+
}
|
|
4973
|
+
});
|
|
3621
4974
|
}
|
|
3622
4975
|
}
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
}
|
|
4976
|
+
if (checkpoint.session?.action === "handoff" || checkpoint.session?.action === "end" || checkpoint.session?.action === "complete_ticket") {
|
|
4977
|
+
if (targetSessionId) {
|
|
4978
|
+
await client.sessions.end({
|
|
4979
|
+
projectId,
|
|
4980
|
+
sessionId: targetSessionId,
|
|
4981
|
+
session: {
|
|
4982
|
+
summary: checkpoint.session.summary ?? summary,
|
|
4983
|
+
...commandMetadata()
|
|
4984
|
+
}
|
|
4985
|
+
});
|
|
4986
|
+
lifecycleOperations.push(checkpointLifecycleOperation("session.ended", true, { sessionId: targetSessionId, action: checkpoint.session.action }));
|
|
3633
4987
|
}
|
|
3634
|
-
}
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
4988
|
+
}
|
|
4989
|
+
if (checkpoint.session?.action === "complete_ticket") {
|
|
4990
|
+
const latestTicket = await client.tickets.get({ projectId, ticketId: targetTicketId });
|
|
4991
|
+
if (typeof latestTicket.projectionVersion !== "number") {
|
|
4992
|
+
checkpoint = checkpointPlanNeedsHuman(checkpoint, "Ticket completion requires a latest ticket projection version.");
|
|
4993
|
+
return await finishCheckpoint("needs_human", checkpoint.route.reason);
|
|
4994
|
+
}
|
|
4995
|
+
await client.tickets.complete({
|
|
4996
|
+
projectId,
|
|
4997
|
+
ticketId: targetTicketId,
|
|
4998
|
+
ifVersion: latestTicket.projectionVersion,
|
|
4999
|
+
completion: {
|
|
5000
|
+
currentSummary: checkpoint.session.summary ?? checkpoint.summaryUpdate?.value ?? summary,
|
|
5001
|
+
...commandMetadata()
|
|
5002
|
+
}
|
|
5003
|
+
});
|
|
5004
|
+
cadenceWrites.push({
|
|
5005
|
+
type: "ticket.completed",
|
|
5006
|
+
success: true,
|
|
5007
|
+
ticketId: targetTicketId,
|
|
5008
|
+
ifVersion: latestTicket.projectionVersion,
|
|
5009
|
+
summary: checkpoint.session.summary ?? checkpoint.summaryUpdate?.value ?? summary
|
|
5010
|
+
});
|
|
5011
|
+
}
|
|
5012
|
+
return await finishCheckpoint("checkpointed");
|
|
3649
5013
|
} finally {
|
|
3650
5014
|
await releaseAgentLoopLock(lockPath);
|
|
3651
5015
|
}
|
|
3652
5016
|
}
|
|
3653
|
-
async function runAgentRunSweep(parsed, options, meta) {
|
|
5017
|
+
async function runAgentRunSweep(parsed, options, config, meta) {
|
|
3654
5018
|
const state = await readAgentLoopState(parsed, options);
|
|
3655
5019
|
const idleAfterSeconds = parsePositiveInteger(parsed.options["idle-after-seconds"], "--idle-after-seconds") ?? 5 * 60;
|
|
3656
5020
|
const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
|
|
@@ -3669,10 +5033,35 @@ async function runAgentRunSweep(parsed, options, meta) {
|
|
|
3669
5033
|
lastObservedAt: session.lastObservedAt
|
|
3670
5034
|
}));
|
|
3671
5035
|
if (!dryRun) {
|
|
5036
|
+
const needsFallbackContext = staleSessions.some((staleSession) => !state.sessions[staleSession.agentSessionKey]?.cadenceContext);
|
|
5037
|
+
let fallbackContext;
|
|
5038
|
+
if (needsFallbackContext && config.projectId) {
|
|
5039
|
+
try {
|
|
5040
|
+
const client = await createClient(config, options);
|
|
5041
|
+
fallbackContext = await readCurrentCadenceContext(client, config.projectId);
|
|
5042
|
+
} catch {}
|
|
5043
|
+
}
|
|
5044
|
+
let nextState = state;
|
|
3672
5045
|
for (const staleSession of staleSessions) {
|
|
3673
|
-
|
|
5046
|
+
const existingSession = nextState.sessions[staleSession.agentSessionKey];
|
|
5047
|
+
const cadenceContext = existingSession?.cadenceContext ?? fallbackContext;
|
|
5048
|
+
if (existingSession && cadenceContext) {
|
|
5049
|
+
nextState = {
|
|
5050
|
+
...nextState,
|
|
5051
|
+
sessions: {
|
|
5052
|
+
...nextState.sessions,
|
|
5053
|
+
[staleSession.agentSessionKey]: {
|
|
5054
|
+
...existingSession,
|
|
5055
|
+
cadenceContext,
|
|
5056
|
+
lastAction: "sweep_spawned"
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
};
|
|
5060
|
+
await writeAgentLoopState(parsed, options, nextState);
|
|
5061
|
+
}
|
|
5062
|
+
await spawnAgentRunCheckpoint([
|
|
3674
5063
|
"agent-run",
|
|
3675
|
-
"
|
|
5064
|
+
"checkpoint",
|
|
3676
5065
|
"--agent-session-key",
|
|
3677
5066
|
staleSession.agentSessionKey,
|
|
3678
5067
|
"--reason",
|
|
@@ -4118,6 +5507,7 @@ async function runIntakeCommand(parsed, options) {
|
|
|
4118
5507
|
...parsed.options.changeset ? { changesetId: parsed.options.changeset } : {},
|
|
4119
5508
|
...parsed.options.actor ? { actorId: parsed.options.actor } : {},
|
|
4120
5509
|
expiresAt: leaseExpiresAt(parsePositiveInteger(parsed.options["ttl-seconds"], "--ttl-seconds") ?? defaultLeaseTtlSeconds),
|
|
5510
|
+
...parseBooleanOption(parsed.options["replace-own-active"], false) ? { replaceOwnActiveLease: true } : {},
|
|
4121
5511
|
...commandMetadata()
|
|
4122
5512
|
}
|
|
4123
5513
|
});
|
|
@@ -4289,8 +5679,27 @@ async function runCli(argv, options = {}) {
|
|
|
4289
5679
|
try {
|
|
4290
5680
|
const parsed = parseCliArgs(argv);
|
|
4291
5681
|
const meta = {
|
|
4292
|
-
command: parsed.command.name
|
|
5682
|
+
command: parsed.flags.version ? "version" : parsed.command.name
|
|
4293
5683
|
};
|
|
5684
|
+
if (parsed.flags.version || parsed.command.name === "version") {
|
|
5685
|
+
const data = {
|
|
5686
|
+
name: package_default.name,
|
|
5687
|
+
version: cliVersion
|
|
5688
|
+
};
|
|
5689
|
+
if (parsed.flags.json) {
|
|
5690
|
+
return {
|
|
5691
|
+
stdout: formatJson(successEnvelope(data, meta)),
|
|
5692
|
+
stderr: "",
|
|
5693
|
+
exitCode: 0
|
|
5694
|
+
};
|
|
5695
|
+
}
|
|
5696
|
+
return {
|
|
5697
|
+
stdout: `cadence ${cliVersion}
|
|
5698
|
+
`,
|
|
5699
|
+
stderr: "",
|
|
5700
|
+
exitCode: 0
|
|
5701
|
+
};
|
|
5702
|
+
}
|
|
4294
5703
|
if (parsed.flags.help || parsed.command.name === "help") {
|
|
4295
5704
|
if (parsed.flags.json) {
|
|
4296
5705
|
return {
|
|
@@ -4324,7 +5733,7 @@ async function runCli(argv, options = {}) {
|
|
|
4324
5733
|
if (parsed.command.name === "projects.list") {
|
|
4325
5734
|
return await runProjectCommand(parsed, options);
|
|
4326
5735
|
}
|
|
4327
|
-
if (parsed.command.name === "agent-run.ingest-stop" || parsed.command.name === "agent-run.closeout" || parsed.command.name === "agent-run.sweep" || parsed.command.name === "agent-run.doctor") {
|
|
5736
|
+
if (parsed.command.name === "agent-run.ingest-stop" || parsed.command.name === "agent-run.checkpoint" || parsed.command.name === "agent-run.closeout" || parsed.command.name === "agent-run.sweep" || parsed.command.name === "agent-run.doctor") {
|
|
4328
5737
|
return await runAgentRunCommand(parsed, options);
|
|
4329
5738
|
}
|
|
4330
5739
|
if (parsed.command.name === "hooks.install" || parsed.command.name === "hooks.doctor") {
|