@trycadence/cli 0.1.11-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 +1491 -141
- package/package.json +1 -1
package/dist/cadence
CHANGED
|
@@ -1513,13 +1513,14 @@ 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";
|
|
1519
1520
|
// package.json
|
|
1520
1521
|
var package_default = {
|
|
1521
1522
|
name: "@trycadence/cli",
|
|
1522
|
-
version: "0.1.
|
|
1523
|
+
version: "0.1.14-dev.0",
|
|
1523
1524
|
private: false,
|
|
1524
1525
|
type: "module",
|
|
1525
1526
|
bin: {
|
|
@@ -1553,14 +1554,13 @@ var workLogParentSelectors = ["last", "ticket-last", "session-last", "last-decis
|
|
|
1553
1554
|
var changesetPrNoteSources = ["agent", "human", "system"];
|
|
1554
1555
|
var hookScopes = ["repo", "global", "both"];
|
|
1555
1556
|
var agentEventSources = ["codex", "claude-code", "opencode", "openrouter", "unknown"];
|
|
1556
|
-
var defaultLeaseTtlSeconds =
|
|
1557
|
+
var defaultLeaseTtlSeconds = 15 * 60;
|
|
1557
1558
|
var defaultCliApiBaseUrl = "https://cadenceapi.deploy.lvl8studios.com";
|
|
1558
1559
|
var defaultCliWebBaseUrl = "https://cadence.deploy.lvl8studios.com";
|
|
1559
|
-
var
|
|
1560
|
-
var defaultCheckpointThresholdMax = 5;
|
|
1560
|
+
var defaultCheckpointThreshold = 3;
|
|
1561
1561
|
var defaultCheckpointCooldownSeconds = 10 * 60;
|
|
1562
1562
|
var defaultCheckpointWorkerTimeoutMs = 10 * 60 * 1000;
|
|
1563
|
-
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'`;
|
|
1564
1564
|
var agentLoopSuppressEnv = "CADENCE_AGENT_EVENT_SUPPRESS";
|
|
1565
1565
|
var credentialRefreshSkewMs = 60 * 1000;
|
|
1566
1566
|
var credentialRefreshLockTimeoutMs = 10 * 1000;
|
|
@@ -1599,6 +1599,7 @@ var knownCommandPaths = [
|
|
|
1599
1599
|
["changesets", "notes", "put"],
|
|
1600
1600
|
["changesets", "notes", "apply"],
|
|
1601
1601
|
["agent-run", "ingest-stop"],
|
|
1602
|
+
["agent-run", "checkpoint"],
|
|
1602
1603
|
["agent-run", "closeout"],
|
|
1603
1604
|
["agent-run", "sweep"],
|
|
1604
1605
|
["agent-run", "doctor"],
|
|
@@ -2260,7 +2261,7 @@ function helpText() {
|
|
|
2260
2261
|
" cadence tickets attach <ticket-id> --from-intake <intake-id> --if-version <version> [--project <project-id>] [--json]",
|
|
2261
2262
|
" cadence tickets create --title <text> [--from-intake <intake-id>] [--project <project-id>] [--json]",
|
|
2262
2263
|
" cadence tickets update <ticket-id> --if-version <version> [--title <text>] [--description <text>] [--priority <priority>] [--status <status>] [--project <project-id>] [--json]",
|
|
2263
|
-
" 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]",
|
|
2264
2265
|
" cadence tickets release <ticket-id> --lease <lease-id> [--project <project-id>] [--json]",
|
|
2265
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]",
|
|
2266
2267
|
" cadence tickets complete <ticket-id> --if-version <version> [--summary <summary>] [--project <project-id>] [--json]",
|
|
@@ -2278,11 +2279,11 @@ function helpText() {
|
|
|
2278
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]",
|
|
2279
2280
|
" cadence changesets notes apply [--changeset <id>|--branch current|<branch>] --provider github --pr-number <n> --pr-url <url> [--project <project-id>] [--json]",
|
|
2280
2281
|
" cadence agent-run ingest-stop --source <codex|claude-code|opencode|openrouter> [--event <event>] [--threshold <n>] [--dry-run true|false] [--project <project-id>] [--json]",
|
|
2281
|
-
" 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]",
|
|
2282
2283
|
" cadence agent-run sweep [--idle-after-seconds <n>] [--dry-run true|false] [--json]",
|
|
2283
2284
|
" cadence agent-run doctor [--json]",
|
|
2284
|
-
" cadence hooks install --provider codex --scope
|
|
2285
|
-
" 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]",
|
|
2286
2287
|
" cadence events list [--project <project-id>] [--ticket <ticket-id>] [--changeset <changeset-id>] [--session <session-id>] [--json]",
|
|
2287
2288
|
"",
|
|
2288
2289
|
"Global flags:",
|
|
@@ -2581,6 +2582,14 @@ function truncateText(value, maxLength) {
|
|
|
2581
2582
|
return `${value.slice(0, maxLength - 15)}
|
|
2582
2583
|
[truncated]`;
|
|
2583
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
|
+
}
|
|
2584
2593
|
function stableHash(value) {
|
|
2585
2594
|
return createHash("sha256").update(value).digest("hex");
|
|
2586
2595
|
}
|
|
@@ -2619,13 +2628,392 @@ function firstString(record, paths) {
|
|
|
2619
2628
|
}
|
|
2620
2629
|
return;
|
|
2621
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
|
+
}
|
|
2622
3010
|
function normalizeAgentEvent(input, parsed, options) {
|
|
2623
3011
|
const source = parseAgentEventSource(parsed.options.source);
|
|
2624
3012
|
const event = parsed.options.event ?? "stop";
|
|
2625
3013
|
const base = normalizeAgentEventBase(input, source, event, options);
|
|
2626
3014
|
switch (source) {
|
|
2627
3015
|
case "codex":
|
|
2628
|
-
return normalizeCodexAgentEvent(input, base);
|
|
3016
|
+
return normalizeCodexAgentEvent(input, base, options);
|
|
2629
3017
|
case "claude-code":
|
|
2630
3018
|
case "opencode":
|
|
2631
3019
|
case "openrouter":
|
|
@@ -2636,6 +3024,7 @@ function normalizeAgentEvent(input, parsed, options) {
|
|
|
2636
3024
|
function normalizeAgentEventBase(input, source, event, options) {
|
|
2637
3025
|
const threadId = firstString(input, [["thread_id"], ["threadId"], ["conversation_id"], ["conversationId"]]);
|
|
2638
3026
|
const turnId = firstString(input, [["turn_id"], ["turnId"], ["id"]]);
|
|
3027
|
+
const recentTurns = readRecentAgentTurns(input);
|
|
2639
3028
|
const lastAssistantMessage = firstString(input, [
|
|
2640
3029
|
["last_assistant_message"],
|
|
2641
3030
|
["lastAssistantMessage"],
|
|
@@ -2652,10 +3041,11 @@ function normalizeAgentEventBase(input, source, event, options) {
|
|
|
2652
3041
|
...threadId ? { threadId } : {},
|
|
2653
3042
|
...turnId ? { turnId } : {},
|
|
2654
3043
|
...lastAssistantMessage ? { lastAssistantMessage: truncateText(lastAssistantMessage, 6000) } : {},
|
|
3044
|
+
...recentTurns ? { recentTurns } : {},
|
|
2655
3045
|
payloadKeys: Object.keys(input).sort()
|
|
2656
3046
|
};
|
|
2657
3047
|
}
|
|
2658
|
-
function normalizeCodexAgentEvent(input, base) {
|
|
3048
|
+
function normalizeCodexAgentEvent(input, base, options) {
|
|
2659
3049
|
const agentSessionId = firstString(input, [["session_id"], ["sessionId"], ["session", "id"]]);
|
|
2660
3050
|
if (!agentSessionId) {
|
|
2661
3051
|
return {
|
|
@@ -2663,10 +3053,13 @@ function normalizeCodexAgentEvent(input, base) {
|
|
|
2663
3053
|
diagnosticReason: "missing_agent_session_id"
|
|
2664
3054
|
};
|
|
2665
3055
|
}
|
|
3056
|
+
const transcriptPath = firstString(input, [["transcript_path"], ["transcriptPath"]]);
|
|
3057
|
+
const recentTurns = base.recentTurns?.length ? base.recentTurns : transcriptPath ? readRecentTurnsFromCodexSessionFile(transcriptPath) : readRecentTurnsFromCodexSession(agentSessionId, options);
|
|
2666
3058
|
return {
|
|
2667
3059
|
...base,
|
|
2668
3060
|
agentSessionId,
|
|
2669
|
-
agentSessionKey: agentSessionKey(base.source, agentSessionId)
|
|
3061
|
+
agentSessionKey: agentSessionKey(base.source, agentSessionId),
|
|
3062
|
+
...recentTurns?.length ? { recentTurns } : {}
|
|
2670
3063
|
};
|
|
2671
3064
|
}
|
|
2672
3065
|
function normalizeGenericAgentEvent(input, base) {
|
|
@@ -2692,14 +3085,23 @@ function normalizeGenericAgentEvent(input, base) {
|
|
|
2692
3085
|
function agentSessionKey(source, agentSessionId) {
|
|
2693
3086
|
return `${source}:${stableHash(agentSessionId)}`;
|
|
2694
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
|
+
}
|
|
2695
3097
|
function defaultAgentLoopState() {
|
|
2696
3098
|
return {
|
|
2697
3099
|
version: 2,
|
|
2698
3100
|
sessions: {}
|
|
2699
3101
|
};
|
|
2700
3102
|
}
|
|
2701
|
-
function
|
|
2702
|
-
return
|
|
3103
|
+
function defaultCheckpointThresholdValue() {
|
|
3104
|
+
return defaultCheckpointThreshold;
|
|
2703
3105
|
}
|
|
2704
3106
|
function agentLoopDirectory(parsed, options) {
|
|
2705
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");
|
|
@@ -2707,8 +3109,81 @@ function agentLoopDirectory(parsed, options) {
|
|
|
2707
3109
|
function agentLoopStatePath(parsed, options) {
|
|
2708
3110
|
return join(agentLoopDirectory(parsed, options), "state.json");
|
|
2709
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
|
+
}
|
|
2710
3121
|
function agentLoopLockPath(parsed, options, agentSessionKeyValue) {
|
|
2711
|
-
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
|
+
};
|
|
2712
3187
|
}
|
|
2713
3188
|
async function readAgentLoopState(parsed, options) {
|
|
2714
3189
|
const filePath = agentLoopStatePath(parsed, options);
|
|
@@ -2754,24 +3229,45 @@ function readAgentLoopSessions(rawSessions) {
|
|
|
2754
3229
|
const record = rawSession;
|
|
2755
3230
|
const source = parseAgentEventSource(typeof record.source === "string" ? record.source : undefined);
|
|
2756
3231
|
const stopCount = typeof record.stopCount === "number" && Number.isInteger(record.stopCount) && record.stopCount >= 0 ? record.stopCount : 0;
|
|
2757
|
-
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;
|
|
2758
3236
|
sessions[key] = {
|
|
2759
3237
|
source,
|
|
2760
3238
|
stopCount,
|
|
2761
3239
|
threshold,
|
|
3240
|
+
...readAgentSessionCadenceContext(record),
|
|
2762
3241
|
...typeof record.firstObservedAt === "string" ? { firstObservedAt: record.firstObservedAt } : {},
|
|
2763
3242
|
...typeof record.lastObservedAt === "string" ? { lastObservedAt: record.lastObservedAt } : {},
|
|
3243
|
+
...typeof record.lastObservedTurnId === "string" ? { lastObservedTurnId: record.lastObservedTurnId } : {},
|
|
2764
3244
|
...typeof record.lastAction === "string" ? { lastAction: record.lastAction } : {},
|
|
2765
3245
|
...typeof record.lastReason === "string" ? { lastReason: record.lastReason } : {},
|
|
2766
3246
|
...typeof record.lastEventFile === "string" ? { lastEventFile: record.lastEventFile } : {},
|
|
2767
3247
|
...typeof record.lastAssistantMessage === "string" ? { lastAssistantMessage: record.lastAssistantMessage } : {},
|
|
3248
|
+
...recentTurns ? { recentTurns } : {},
|
|
2768
3249
|
...typeof record.lastCheckpointAt === "string" ? { lastCheckpointAt: record.lastCheckpointAt } : {},
|
|
2769
3250
|
...typeof record.lastCheckpointFingerprint === "string" ? { lastCheckpointFingerprint: record.lastCheckpointFingerprint } : {},
|
|
2770
|
-
...typeof record.previousCheckpointSummary === "string" ? { previousCheckpointSummary: record.previousCheckpointSummary } : {}
|
|
3251
|
+
...typeof record.previousCheckpointSummary === "string" ? { previousCheckpointSummary: record.previousCheckpointSummary } : {},
|
|
3252
|
+
...typeof record.lastCheckpointAuditFile === "string" ? { lastCheckpointAuditFile: record.lastCheckpointAuditFile } : {}
|
|
2771
3253
|
};
|
|
2772
3254
|
}
|
|
2773
3255
|
return sessions;
|
|
2774
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
|
+
}
|
|
2775
3271
|
function readAgentLoopDiagnostics(record) {
|
|
2776
3272
|
return {
|
|
2777
3273
|
...typeof record.missingSessionIdCount === "number" && Number.isInteger(record.missingSessionIdCount) && record.missingSessionIdCount > 0 ? { missingSessionIdCount: record.missingSessionIdCount } : {},
|
|
@@ -2806,6 +3302,15 @@ async function writeAgentEventFile(parsed, options, event) {
|
|
|
2806
3302
|
`);
|
|
2807
3303
|
return filePath;
|
|
2808
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
|
+
}
|
|
2809
3314
|
async function removeStaleAgentLoopLock(lockPath) {
|
|
2810
3315
|
try {
|
|
2811
3316
|
const lockStats = await stat(lockPath);
|
|
@@ -2891,7 +3396,7 @@ function currentCliWorkerInvocation() {
|
|
|
2891
3396
|
argsPrefix: []
|
|
2892
3397
|
};
|
|
2893
3398
|
}
|
|
2894
|
-
async function
|
|
3399
|
+
async function spawnAgentRunCheckpoint(args, options) {
|
|
2895
3400
|
const cwd = options.cwd ?? process.cwd();
|
|
2896
3401
|
const invocation = currentCliWorkerInvocation();
|
|
2897
3402
|
const env = {
|
|
@@ -2910,47 +3415,88 @@ async function spawnAgentRunCloseout(args, options) {
|
|
|
2910
3415
|
});
|
|
2911
3416
|
child.unref();
|
|
2912
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
|
+
}
|
|
2913
3431
|
function activeSessionFromCurrent(current) {
|
|
2914
3432
|
const record = current && typeof current === "object" ? current : null;
|
|
2915
3433
|
const sessions = Array.isArray(record?.sessions) ? record.sessions : [];
|
|
2916
|
-
const activeSession = sessions.find((session) => {
|
|
2917
|
-
if (!session || typeof session !== "object") {
|
|
2918
|
-
return false;
|
|
2919
|
-
}
|
|
2920
|
-
const sessionRecord2 = session;
|
|
2921
|
-
return sessionRecord2.status === "active" && typeof sessionRecord2.ticketId === "string";
|
|
2922
|
-
});
|
|
2923
|
-
if (activeSession && typeof activeSession === "object") {
|
|
2924
|
-
return activeSession;
|
|
2925
|
-
}
|
|
2926
3434
|
const activeLeases = Array.isArray(record?.activeLeases) ? record.activeLeases : Array.isArray(record?.leases) ? record.leases.filter((lease) => lease && typeof lease === "object" && lease.status === "active") : [];
|
|
2927
|
-
const activeLease = activeLeases.
|
|
3435
|
+
const activeLease = activeLeases.filter((lease) => {
|
|
2928
3436
|
if (!lease || typeof lease !== "object") {
|
|
2929
3437
|
return false;
|
|
2930
3438
|
}
|
|
2931
|
-
const
|
|
2932
|
-
return typeof
|
|
2933
|
-
});
|
|
2934
|
-
if (
|
|
2935
|
-
|
|
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
|
+
};
|
|
2936
3459
|
}
|
|
2937
|
-
const
|
|
2938
|
-
const sessionId = typeof leaseRecord.sessionId === "string" ? leaseRecord.sessionId : undefined;
|
|
2939
|
-
const matchingSession = sessions.find((session) => {
|
|
3460
|
+
const activeSessions = sessions.filter((session) => {
|
|
2940
3461
|
if (!session || typeof session !== "object") {
|
|
2941
3462
|
return false;
|
|
2942
3463
|
}
|
|
2943
|
-
|
|
3464
|
+
const sessionRecord = session;
|
|
3465
|
+
return sessionRecord.status === "active" && typeof sessionRecord.ticketId === "string";
|
|
2944
3466
|
});
|
|
2945
|
-
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
|
+
}
|
|
2946
3484
|
return {
|
|
2947
|
-
...
|
|
2948
|
-
...sessionId ? {
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
status: "active"
|
|
3485
|
+
...ticketId ? { ticketId } : {},
|
|
3486
|
+
...sessionId ? { sessionId } : {},
|
|
3487
|
+
...changesetId ? { changesetId } : {},
|
|
3488
|
+
capturedAt: new Date().toISOString()
|
|
2952
3489
|
};
|
|
2953
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
|
+
}
|
|
2954
3500
|
function fingerprintForCheckpoint(event, options) {
|
|
2955
3501
|
const status = gitOutput(["status", "--short"], options);
|
|
2956
3502
|
const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
|
|
@@ -2958,6 +3504,7 @@ function fingerprintForCheckpoint(event, options) {
|
|
|
2958
3504
|
source: event.source,
|
|
2959
3505
|
event: event.event,
|
|
2960
3506
|
lastAssistantMessage: event.lastAssistantMessage ?? "",
|
|
3507
|
+
recentTurns: event.recentTurns ?? [],
|
|
2961
3508
|
status,
|
|
2962
3509
|
changedFiles
|
|
2963
3510
|
}));
|
|
@@ -2972,24 +3519,106 @@ function shouldSkipForCooldown(state, cooldownSeconds) {
|
|
|
2972
3519
|
function synthesizeAgentEventFromSession(agentSessionKeyValue, session, options) {
|
|
2973
3520
|
return {
|
|
2974
3521
|
source: session.source,
|
|
2975
|
-
event: "
|
|
3522
|
+
event: "checkpoint",
|
|
2976
3523
|
workspacePath: options.cwd ?? process.cwd(),
|
|
2977
3524
|
occurredAt: new Date().toISOString(),
|
|
2978
3525
|
agentSessionKey: agentSessionKeyValue,
|
|
2979
3526
|
...session.lastAssistantMessage ? { lastAssistantMessage: session.lastAssistantMessage } : {},
|
|
3527
|
+
...session.recentTurns?.length ? { recentTurns: session.recentTurns } : {},
|
|
2980
3528
|
payloadKeys: []
|
|
2981
3529
|
};
|
|
2982
3530
|
}
|
|
2983
|
-
|
|
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();
|
|
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());
|
|
3554
|
+
} catch {
|
|
3555
|
+
return {};
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
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) {
|
|
2984
3617
|
const trimmed = raw.trim();
|
|
2985
3618
|
try {
|
|
2986
3619
|
const parsed = JSON.parse(trimmed);
|
|
2987
3620
|
if (parsed && typeof parsed === "object") {
|
|
2988
|
-
|
|
2989
|
-
return {
|
|
2990
|
-
body: typeof record.body === "string" && record.body.trim() ? record.body.trim() : trimmed,
|
|
2991
|
-
summary: typeof record.summary === "string" && record.summary.trim() ? record.summary.trim() : undefined
|
|
2992
|
-
};
|
|
3621
|
+
return parseCheckpointRecord(parsed, trimmed, fallbackKind);
|
|
2993
3622
|
}
|
|
2994
3623
|
} catch {
|
|
2995
3624
|
const start = trimmed.indexOf("{");
|
|
@@ -2998,34 +3627,378 @@ function parseCheckpointJson(raw) {
|
|
|
2998
3627
|
try {
|
|
2999
3628
|
const parsed = JSON.parse(trimmed.slice(start, end + 1));
|
|
3000
3629
|
if (parsed && typeof parsed === "object") {
|
|
3001
|
-
|
|
3002
|
-
return {
|
|
3003
|
-
body: typeof record.body === "string" && record.body.trim() ? record.body.trim() : trimmed,
|
|
3004
|
-
summary: typeof record.summary === "string" && record.summary.trim() ? record.summary.trim() : undefined
|
|
3005
|
-
};
|
|
3630
|
+
return parseCheckpointRecord(parsed, trimmed, fallbackKind);
|
|
3006
3631
|
}
|
|
3007
3632
|
} catch {}
|
|
3008
3633
|
}
|
|
3009
3634
|
}
|
|
3010
3635
|
return {
|
|
3011
|
-
|
|
3012
|
-
|
|
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
|
+
} : {}
|
|
3013
3975
|
};
|
|
3014
3976
|
}
|
|
3015
3977
|
function buildCheckpointPrompt(input) {
|
|
3978
|
+
const recentTurns = checkpointRecentTurns(input.event);
|
|
3979
|
+
const recentTurnsText = recentTurns.length ? formatCheckpointRecentTurns(recentTurns) : "";
|
|
3016
3980
|
return [
|
|
3017
|
-
"You are generating a
|
|
3018
|
-
"
|
|
3019
|
-
|
|
3020
|
-
"
|
|
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.",
|
|
3021
3993
|
"",
|
|
3022
3994
|
`Ticket: ${input.ticketId}`,
|
|
3023
3995
|
input.sessionId ? `Session: ${input.sessionId}` : "",
|
|
3024
3996
|
input.changesetId ? `ChangeSet: ${input.changesetId}` : "",
|
|
3025
3997
|
`Agent event: ${input.event.source}/${input.event.event}`,
|
|
3026
3998
|
input.previousCheckpointSummary ? `Previous checkpoint summary: ${input.previousCheckpointSummary}` : "",
|
|
3027
|
-
|
|
3028
|
-
${
|
|
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",
|
|
3029
4002
|
input.gitStatus ? `Git status --short:
|
|
3030
4003
|
${truncateText(input.gitStatus, 2000)}` : "Git status --short: clean or unavailable",
|
|
3031
4004
|
input.gitDiffStat ? `Git diff --stat origin/dev...:
|
|
@@ -3058,6 +4031,22 @@ function codexHookEntry(command) {
|
|
|
3058
4031
|
]
|
|
3059
4032
|
};
|
|
3060
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
|
+
}
|
|
3061
4050
|
async function readJsonObjectFile(filePath) {
|
|
3062
4051
|
const file = Bun.file(filePath);
|
|
3063
4052
|
if (!await file.exists()) {
|
|
@@ -3071,8 +4060,9 @@ async function installCodexHookFile(filePath, command) {
|
|
|
3071
4060
|
const hooks = hooksValue && typeof hooksValue === "object" && !Array.isArray(hooksValue) ? hooksValue : {};
|
|
3072
4061
|
const stopValue = hooks.Stop;
|
|
3073
4062
|
const stopHooks = Array.isArray(stopValue) ? stopValue : [];
|
|
3074
|
-
const alreadyInstalled = stopHooks.some((entry) =>
|
|
3075
|
-
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)];
|
|
3076
4066
|
const nextConfig = {
|
|
3077
4067
|
...existing,
|
|
3078
4068
|
hooks: {
|
|
@@ -3086,7 +4076,8 @@ async function installCodexHookFile(filePath, command) {
|
|
|
3086
4076
|
return {
|
|
3087
4077
|
path: filePath,
|
|
3088
4078
|
installed: !alreadyInstalled,
|
|
3089
|
-
alreadyInstalled
|
|
4079
|
+
alreadyInstalled,
|
|
4080
|
+
updated: !alreadyInstalled && cadenceHookIndexes.length > 0
|
|
3090
4081
|
};
|
|
3091
4082
|
}
|
|
3092
4083
|
async function codexHookPaths(scope, options) {
|
|
@@ -3108,7 +4099,7 @@ async function codexHookInstalled(filePath, command) {
|
|
|
3108
4099
|
const existing = await readJsonObjectFile(filePath);
|
|
3109
4100
|
const hooks = existing.hooks;
|
|
3110
4101
|
const stopHooks = hooks && typeof hooks === "object" && !Array.isArray(hooks) && Array.isArray(hooks.Stop) ? hooks.Stop : [];
|
|
3111
|
-
return stopHooks.some((entry) =>
|
|
4102
|
+
return stopHooks.some((entry) => codexHookCommands(entry).includes(command));
|
|
3112
4103
|
}
|
|
3113
4104
|
async function runStatus(parsed, options) {
|
|
3114
4105
|
const config = await resolveCliConfig(parsed.flags, options);
|
|
@@ -3255,14 +4246,17 @@ async function runAgentRunCommand(parsed, options) {
|
|
|
3255
4246
|
switch (parsed.command.name) {
|
|
3256
4247
|
case "agent-run.ingest-stop":
|
|
3257
4248
|
return await runAgentRunIngestStop(parsed, options, config, meta);
|
|
4249
|
+
case "agent-run.checkpoint":
|
|
3258
4250
|
case "agent-run.closeout":
|
|
3259
|
-
return await
|
|
4251
|
+
return await runAgentRunCheckpoint(parsed, options, config, meta);
|
|
3260
4252
|
case "agent-run.sweep":
|
|
3261
|
-
return await runAgentRunSweep(parsed, options, meta);
|
|
4253
|
+
return await runAgentRunSweep(parsed, options, config, meta);
|
|
3262
4254
|
case "agent-run.doctor": {
|
|
3263
4255
|
const state = await readAgentLoopState(parsed, options);
|
|
4256
|
+
const settings = await readAgentLoopSettings(parsed, options);
|
|
3264
4257
|
const data = {
|
|
3265
4258
|
action: "doctor",
|
|
4259
|
+
settings,
|
|
3266
4260
|
state,
|
|
3267
4261
|
sessionCount: Object.keys(state.sessions).length,
|
|
3268
4262
|
pendingSessions: Object.entries(state.sessions).filter(([, session]) => session.stopCount > 0).map(([agentSessionKeyValue, session]) => ({
|
|
@@ -3337,8 +4331,15 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
3337
4331
|
const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
|
|
3338
4332
|
const existingSession = state.sessions[normalized.agentSessionKey];
|
|
3339
4333
|
const now = new Date().toISOString();
|
|
3340
|
-
const threshold = forcedThreshold ?? existingSession?.threshold ??
|
|
3341
|
-
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 {}
|
|
3342
4343
|
const observedSession = {
|
|
3343
4344
|
...clearAgentSessionReason(existingSession ?? {
|
|
3344
4345
|
source: normalized.source,
|
|
@@ -3350,8 +4351,11 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
3350
4351
|
threshold,
|
|
3351
4352
|
firstObservedAt: existingSession?.firstObservedAt ?? now,
|
|
3352
4353
|
lastObservedAt: now,
|
|
4354
|
+
...normalized.turnId ? { lastObservedTurnId: normalized.turnId } : {},
|
|
3353
4355
|
lastAction: "counted",
|
|
3354
|
-
...
|
|
4356
|
+
...cadenceContext ? { cadenceContext } : {},
|
|
4357
|
+
...normalized.lastAssistantMessage ? { lastAssistantMessage: normalized.lastAssistantMessage } : {},
|
|
4358
|
+
...recentTurns ? { recentTurns } : {}
|
|
3355
4359
|
};
|
|
3356
4360
|
const countedState = {
|
|
3357
4361
|
...state,
|
|
@@ -3363,7 +4367,8 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
3363
4367
|
if (nextCount < threshold) {
|
|
3364
4368
|
await writeAgentLoopState(parsed, options, countedState);
|
|
3365
4369
|
const data2 = {
|
|
3366
|
-
action: "counted",
|
|
4370
|
+
action: duplicateTurn ? "updated" : "counted",
|
|
4371
|
+
...duplicateTurn ? { reason: "duplicate_turn" } : {},
|
|
3367
4372
|
agentSessionKey: normalized.agentSessionKey,
|
|
3368
4373
|
stopCount: nextCount,
|
|
3369
4374
|
threshold
|
|
@@ -3410,7 +4415,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
3410
4415
|
[normalized.agentSessionKey]: {
|
|
3411
4416
|
...observedSession,
|
|
3412
4417
|
stopCount: 0,
|
|
3413
|
-
threshold:
|
|
4418
|
+
threshold: defaultCheckpointThresholdValue(),
|
|
3414
4419
|
lastAction: "skipped",
|
|
3415
4420
|
lastReason: "unchanged"
|
|
3416
4421
|
}
|
|
@@ -3432,7 +4437,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
3432
4437
|
const lockPath = agentLoopLockPath(parsed, options, normalized.agentSessionKey);
|
|
3433
4438
|
const workerArgs = [
|
|
3434
4439
|
"agent-run",
|
|
3435
|
-
"
|
|
4440
|
+
"checkpoint",
|
|
3436
4441
|
"--agent-session-key",
|
|
3437
4442
|
normalized.agentSessionKey,
|
|
3438
4443
|
"--reason",
|
|
@@ -3498,7 +4503,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
3498
4503
|
};
|
|
3499
4504
|
}
|
|
3500
4505
|
try {
|
|
3501
|
-
await
|
|
4506
|
+
await spawnAgentRunCheckpoint(workerArgs, options);
|
|
3502
4507
|
} catch (error) {
|
|
3503
4508
|
await releaseAgentLoopLock(lock.lockPath);
|
|
3504
4509
|
throw error;
|
|
@@ -3509,8 +4514,9 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
3509
4514
|
...state.sessions,
|
|
3510
4515
|
[normalized.agentSessionKey]: {
|
|
3511
4516
|
...observedSession,
|
|
4517
|
+
...cadenceContext ? { cadenceContext } : {},
|
|
3512
4518
|
stopCount: 0,
|
|
3513
|
-
threshold:
|
|
4519
|
+
threshold: defaultCheckpointThresholdValue(),
|
|
3514
4520
|
lastAction: "spawned",
|
|
3515
4521
|
lastEventFile: eventFile,
|
|
3516
4522
|
lastCheckpointAt: new Date().toISOString(),
|
|
@@ -3531,7 +4537,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
3531
4537
|
exitCode: 0
|
|
3532
4538
|
};
|
|
3533
4539
|
}
|
|
3534
|
-
async function
|
|
4540
|
+
async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
3535
4541
|
const projectId = requireProjectId(config);
|
|
3536
4542
|
const client = await createClient(config, options);
|
|
3537
4543
|
const agentSessionKeyValue = requireOption(parsed, "agent-session-key");
|
|
@@ -3542,16 +4548,11 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
|
|
|
3542
4548
|
agentSessionKey: agentSessionKeyValue
|
|
3543
4549
|
});
|
|
3544
4550
|
}
|
|
3545
|
-
const
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
});
|
|
3551
|
-
const currentSession = activeSessionFromCurrent(current);
|
|
3552
|
-
const ticketId = parsed.options.ticket ?? (currentSession && typeof currentSession.ticketId === "string" ? currentSession.ticketId : undefined);
|
|
3553
|
-
const sessionId = parsed.options.session ?? (currentSession && typeof currentSession.id === "string" ? currentSession.id : undefined);
|
|
3554
|
-
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;
|
|
3555
4556
|
if (!ticketId) {
|
|
3556
4557
|
await writeAgentLoopState(parsed, options, {
|
|
3557
4558
|
...state,
|
|
@@ -3582,6 +4583,7 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
|
|
|
3582
4583
|
const updateSummary = parseBooleanOption(parsed.options["update-summary"], false);
|
|
3583
4584
|
const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
|
|
3584
4585
|
const lockPath = parsed.options.lock;
|
|
4586
|
+
const checkpointSettings = await resolveCheckpointSettings(parsed, options);
|
|
3585
4587
|
const gitStatus = gitOutput(["status", "--short"], options);
|
|
3586
4588
|
const gitDiffStat = gitOutput(["diff", "--stat", "origin/dev..."], options);
|
|
3587
4589
|
const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
|
|
@@ -3595,10 +4597,29 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
|
|
|
3595
4597
|
changedFiles,
|
|
3596
4598
|
...sessionState.previousCheckpointSummary ? { previousCheckpointSummary: sessionState.previousCheckpointSummary } : {}
|
|
3597
4599
|
});
|
|
4600
|
+
const promptTokenAccounting = buildCheckpointTokenAccounting(event, prompt, undefined);
|
|
3598
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
|
+
});
|
|
3599
4619
|
const data = {
|
|
3600
|
-
action: "
|
|
4620
|
+
action: "would_checkpoint",
|
|
3601
4621
|
prompt,
|
|
4622
|
+
auditFile,
|
|
3602
4623
|
ticketId,
|
|
3603
4624
|
agentSessionKey: agentSessionKeyValue,
|
|
3604
4625
|
...sessionId ? { sessionId } : {},
|
|
@@ -3613,84 +4634,387 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
|
|
|
3613
4634
|
}
|
|
3614
4635
|
try {
|
|
3615
4636
|
const codexCommand = parsed.options["codex-command"] ?? "codex";
|
|
3616
|
-
const
|
|
3617
|
-
|
|
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,
|
|
3618
4652
|
env: {
|
|
3619
4653
|
[agentLoopSuppressEnv]: "1",
|
|
3620
4654
|
CADENCE_HOOK_SUPPRESS: "1"
|
|
3621
4655
|
},
|
|
3622
4656
|
timeoutMs: defaultCheckpointWorkerTimeoutMs
|
|
3623
4657
|
});
|
|
4658
|
+
const codexSessionTranscript = readCodexSessionTranscript(findCodexSessionFileCreatedAfter(options, codexStartedAtMs, codexCwd));
|
|
4659
|
+
const tokenAccounting = buildCheckpointTokenAccounting(event, prompt, codexSessionTranscript?.tokenUsage);
|
|
3624
4660
|
if (codex.status !== 0 || codex.error) {
|
|
3625
|
-
throw new CliError("
|
|
4661
|
+
throw new CliError("AGENT_RUN_CHECKPOINT_FAILED", "Codex checkpoint generation failed.", {
|
|
3626
4662
|
status: codex.status,
|
|
3627
4663
|
stderr: truncateText(codex.stderr, 2000),
|
|
3628
4664
|
error: codex.error?.message
|
|
3629
4665
|
});
|
|
3630
4666
|
}
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
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,
|
|
4709
|
+
summary,
|
|
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,
|
|
3643
4742
|
summary,
|
|
3644
|
-
|
|
3645
|
-
|
|
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,
|
|
3646
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
|
+
});
|
|
3647
4929
|
}
|
|
3648
|
-
}
|
|
3649
|
-
if (
|
|
3650
|
-
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 });
|
|
3651
4955
|
if (typeof ticket.projectionVersion === "number") {
|
|
3652
4956
|
await client.tickets.update({
|
|
3653
4957
|
projectId,
|
|
3654
|
-
ticketId,
|
|
4958
|
+
ticketId: targetTicketId,
|
|
3655
4959
|
ifVersion: ticket.projectionVersion,
|
|
3656
4960
|
ticket: {
|
|
3657
|
-
currentSummary:
|
|
4961
|
+
currentSummary: checkpoint.summaryUpdate.value,
|
|
3658
4962
|
...commandMetadata()
|
|
3659
4963
|
}
|
|
3660
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
|
+
});
|
|
3661
4974
|
}
|
|
3662
4975
|
}
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
}
|
|
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 }));
|
|
3673
4987
|
}
|
|
3674
|
-
}
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
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");
|
|
3689
5013
|
} finally {
|
|
3690
5014
|
await releaseAgentLoopLock(lockPath);
|
|
3691
5015
|
}
|
|
3692
5016
|
}
|
|
3693
|
-
async function runAgentRunSweep(parsed, options, meta) {
|
|
5017
|
+
async function runAgentRunSweep(parsed, options, config, meta) {
|
|
3694
5018
|
const state = await readAgentLoopState(parsed, options);
|
|
3695
5019
|
const idleAfterSeconds = parsePositiveInteger(parsed.options["idle-after-seconds"], "--idle-after-seconds") ?? 5 * 60;
|
|
3696
5020
|
const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
|
|
@@ -3709,10 +5033,35 @@ async function runAgentRunSweep(parsed, options, meta) {
|
|
|
3709
5033
|
lastObservedAt: session.lastObservedAt
|
|
3710
5034
|
}));
|
|
3711
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;
|
|
3712
5045
|
for (const staleSession of staleSessions) {
|
|
3713
|
-
|
|
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([
|
|
3714
5063
|
"agent-run",
|
|
3715
|
-
"
|
|
5064
|
+
"checkpoint",
|
|
3716
5065
|
"--agent-session-key",
|
|
3717
5066
|
staleSession.agentSessionKey,
|
|
3718
5067
|
"--reason",
|
|
@@ -4158,6 +5507,7 @@ async function runIntakeCommand(parsed, options) {
|
|
|
4158
5507
|
...parsed.options.changeset ? { changesetId: parsed.options.changeset } : {},
|
|
4159
5508
|
...parsed.options.actor ? { actorId: parsed.options.actor } : {},
|
|
4160
5509
|
expiresAt: leaseExpiresAt(parsePositiveInteger(parsed.options["ttl-seconds"], "--ttl-seconds") ?? defaultLeaseTtlSeconds),
|
|
5510
|
+
...parseBooleanOption(parsed.options["replace-own-active"], false) ? { replaceOwnActiveLease: true } : {},
|
|
4161
5511
|
...commandMetadata()
|
|
4162
5512
|
}
|
|
4163
5513
|
});
|
|
@@ -4383,7 +5733,7 @@ async function runCli(argv, options = {}) {
|
|
|
4383
5733
|
if (parsed.command.name === "projects.list") {
|
|
4384
5734
|
return await runProjectCommand(parsed, options);
|
|
4385
5735
|
}
|
|
4386
|
-
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") {
|
|
4387
5737
|
return await runAgentRunCommand(parsed, options);
|
|
4388
5738
|
}
|
|
4389
5739
|
if (parsed.command.name === "hooks.install" || parsed.command.name === "hooks.doctor") {
|