@gajae-code/coding-agent 0.12.1 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -3
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +1 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +9 -0
- package/dist/types/modes/theme/theme.d.ts +3 -1
- package/dist/types/modes/types.d.ts +32 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -2
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +22 -3
- package/dist/types/task/index.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +1 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/package.json +8 -8
- package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-acp-conformance.ts +5 -0
- package/src/async/job-manager.ts +22 -20
- package/src/config/settings-schema.ts +2 -2
- package/src/debug/index.ts +42 -17
- package/src/defaults/gjc/skills/ralplan/SKILL.md +9 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -2
- package/src/gjc-runtime/launch-worktree.ts +103 -1
- package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
- package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
- package/src/gjc-runtime/ralplan-runtime.ts +80 -10
- package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
- package/src/gjc-runtime/ultragoal-runtime.ts +236 -3
- package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
- package/src/gjc-runtime/workflow-manifest.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/DESIGN.md +53 -0
- package/src/modes/components/irc-sidebar.ts +30 -1
- package/src/modes/components/tool-status-header.ts +0 -1
- package/src/modes/controllers/command-controller.ts +38 -34
- package/src/modes/controllers/event-controller.ts +27 -16
- package/src/modes/controllers/extension-ui-controller.ts +186 -34
- package/src/modes/controllers/selector-controller.ts +115 -22
- package/src/modes/interactive-mode.ts +189 -62
- package/src/modes/jobs-observer.ts +1 -1
- package/src/modes/theme/theme.ts +11 -9
- package/src/modes/types.ts +88 -0
- package/src/sdk/broker/lifecycle.ts +18 -13
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -2
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +1 -0
- package/src/session/agent-session.ts +22 -1
- package/src/session/internal/managed-session-scope.ts +158 -107
- package/src/session/internal/managed-session-storage.ts +445 -57
- package/src/session/session-storage.ts +29 -44
- package/src/task/index.ts +257 -31
- package/src/task/output-manager.ts +24 -14
- package/src/task/receipt.ts +11 -1
- package/src/tools/index.ts +2 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product-surface dogfood for #2902 ralplan typed review conflicts.
|
|
3
|
+
*
|
|
4
|
+
* Uses the **compiled** `packages/coding-agent/dist/gjc` binary (not source
|
|
5
|
+
* `cli.ts`) so evidence matches the owner exact-head compiled-binary gate:
|
|
6
|
+
* 1) open conflicts fail closed (exit 2, Join blocked)
|
|
7
|
+
* 2) complete disposition document is accepted and persisted
|
|
8
|
+
* 3) stored artifact is dispositioned under ralplan.review_conflicts.v1
|
|
9
|
+
* 4) spoofed receipts fail closed against the run index
|
|
10
|
+
*
|
|
11
|
+
* Prerequisites (from monorepo root, same HEAD):
|
|
12
|
+
* bun run build
|
|
13
|
+
* bun packages/coding-agent/scripts/dogfood-ralplan-review-conflicts.ts
|
|
14
|
+
*
|
|
15
|
+
* Optional: GJC_BINARY=/path/to/gjc overrides the default dist path.
|
|
16
|
+
*/
|
|
17
|
+
import * as fsp from "node:fs/promises";
|
|
18
|
+
import * as os from "node:os";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
23
|
+
const defaultBinary = path.join(repoRoot, "packages/coding-agent/dist/gjc");
|
|
24
|
+
|
|
25
|
+
async function resolveCompiledBinary(): Promise<string> {
|
|
26
|
+
const binary = process.env.GJC_BINARY?.trim() || defaultBinary;
|
|
27
|
+
try {
|
|
28
|
+
const st = await fsp.stat(binary);
|
|
29
|
+
if (!st.isFile()) throw new Error(`not a file: ${binary}`);
|
|
30
|
+
// Ensure executable bit is present for direct spawn.
|
|
31
|
+
await fsp.access(binary, fsp.constants.X_OK).catch(async () => {
|
|
32
|
+
await fsp.chmod(binary, 0o755);
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Compiled binary missing or not executable: ${binary}. Run \`bun run build\` on this exact HEAD first. (${error instanceof Error ? error.message : String(error)})`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return binary;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function runGjc(
|
|
43
|
+
binary: string,
|
|
44
|
+
cwd: string,
|
|
45
|
+
args: string[],
|
|
46
|
+
env: NodeJS.ProcessEnv,
|
|
47
|
+
): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
48
|
+
const proc = Bun.spawn([binary, ...args], { cwd, env, stdout: "pipe", stderr: "pipe" });
|
|
49
|
+
const [code, stdout, stderr] = await Promise.all([
|
|
50
|
+
proc.exited,
|
|
51
|
+
new Response(proc.stdout).text(),
|
|
52
|
+
new Response(proc.stderr).text(),
|
|
53
|
+
]);
|
|
54
|
+
return { code, stdout, stderr };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function main(): Promise<void> {
|
|
58
|
+
const binary = await resolveCompiledBinary();
|
|
59
|
+
const dogfoodRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "gjc-dogfood-2902-"));
|
|
60
|
+
const sessionId = `dogfood-2902-${process.pid}`;
|
|
61
|
+
const env = { ...process.env, GJC_SESSION_ID: sessionId };
|
|
62
|
+
const full = Bun.spawnSync(["git", "-C", repoRoot, "rev-parse", "HEAD"]).stdout.toString().trim();
|
|
63
|
+
const short = Bun.spawnSync(["git", "-C", repoRoot, "rev-parse", "--short=8", "HEAD"]).stdout.toString().trim();
|
|
64
|
+
const binaryStat = await fsp.stat(binary);
|
|
65
|
+
|
|
66
|
+
console.log("# Dogfood: ralplan review conflicts (#2902) — compiled binary");
|
|
67
|
+
console.log(`root=${dogfoodRoot}`);
|
|
68
|
+
console.log(`session=${sessionId}`);
|
|
69
|
+
console.log(`binary=${binary}`);
|
|
70
|
+
console.log(`binary_size=${binaryStat.size}`);
|
|
71
|
+
console.log(`binary_mtime=${binaryStat.mtime.toISOString()}`);
|
|
72
|
+
console.log(`commit=${short}`);
|
|
73
|
+
console.log(`commit_full=${full}`);
|
|
74
|
+
console.log(`bun=${Bun.version}`);
|
|
75
|
+
console.log();
|
|
76
|
+
|
|
77
|
+
// Seed ralplan run state so --write has an active run.
|
|
78
|
+
console.log("## 1) compiled gjc ralplan seed");
|
|
79
|
+
const seed = await runGjc(binary, dogfoodRoot, ["ralplan", "--deliberate", "--json", "dogfood #2902"], env);
|
|
80
|
+
console.log(`exit=${seed.code}`);
|
|
81
|
+
console.log((seed.stdout || seed.stderr).trim());
|
|
82
|
+
if (seed.code !== 0) process.exit(1);
|
|
83
|
+
|
|
84
|
+
// Persist same-pass Architect/Critic artifacts so disposition receipts resolve.
|
|
85
|
+
console.log();
|
|
86
|
+
console.log("## 2) seed architect + critic stage artifacts (same-pass stage_n=1)");
|
|
87
|
+
const architect = await runGjc(
|
|
88
|
+
binary,
|
|
89
|
+
dogfoodRoot,
|
|
90
|
+
["ralplan", "--write", "--stage", "architect", "--stage_n", "1", "--artifact", "# architect", "--json"],
|
|
91
|
+
env,
|
|
92
|
+
);
|
|
93
|
+
console.log(`architect exit=${architect.code}`);
|
|
94
|
+
if (architect.code !== 0) {
|
|
95
|
+
console.error(architect.stderr || architect.stdout);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
const critic = await runGjc(
|
|
99
|
+
binary,
|
|
100
|
+
dogfoodRoot,
|
|
101
|
+
["ralplan", "--write", "--stage", "critic", "--stage_n", "1", "--artifact", "# critic", "--json"],
|
|
102
|
+
env,
|
|
103
|
+
);
|
|
104
|
+
console.log(`critic exit=${critic.code}`);
|
|
105
|
+
if (critic.code !== 0) {
|
|
106
|
+
console.error(critic.stderr || critic.stdout);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
const archReceipt = JSON.parse(architect.stdout) as { path: string; sha256: string };
|
|
110
|
+
const critReceipt = JSON.parse(critic.stdout) as { path: string; sha256: string };
|
|
111
|
+
console.log(`architect path=${archReceipt.path}`);
|
|
112
|
+
console.log(`critic path=${critReceipt.path}`);
|
|
113
|
+
|
|
114
|
+
const findings = [
|
|
115
|
+
{
|
|
116
|
+
findingId: "arch-1",
|
|
117
|
+
targetId: "contract.field",
|
|
118
|
+
action: "remove",
|
|
119
|
+
severity: "block",
|
|
120
|
+
evidence: "redundant with session identity",
|
|
121
|
+
sourceRole: "architect",
|
|
122
|
+
sourceReceipt: {
|
|
123
|
+
stage: "architect",
|
|
124
|
+
stageN: 1,
|
|
125
|
+
path: archReceipt.path,
|
|
126
|
+
sha256: archReceipt.sha256,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
findingId: "crit-1",
|
|
131
|
+
targetId: "contract.field",
|
|
132
|
+
action: "add",
|
|
133
|
+
severity: "watch",
|
|
134
|
+
evidence: "needed for multi-repo binding",
|
|
135
|
+
sourceRole: "critic",
|
|
136
|
+
sourceReceipt: {
|
|
137
|
+
stage: "critic",
|
|
138
|
+
stageN: 1,
|
|
139
|
+
path: critReceipt.path,
|
|
140
|
+
sha256: critReceipt.sha256,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
// 3) Open disposition must fail closed.
|
|
146
|
+
const openPath = path.join(dogfoodRoot, "open-disposition.json");
|
|
147
|
+
await fsp.writeFile(
|
|
148
|
+
openPath,
|
|
149
|
+
JSON.stringify({
|
|
150
|
+
schema: "ralplan.review_conflicts.v1",
|
|
151
|
+
plannerStageN: 1,
|
|
152
|
+
findings,
|
|
153
|
+
dispositions: [],
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
console.log();
|
|
157
|
+
console.log("## 3) disposition stage with open conflicts (expect fail-closed)");
|
|
158
|
+
const open = await runGjc(
|
|
159
|
+
binary,
|
|
160
|
+
dogfoodRoot,
|
|
161
|
+
["ralplan", "--write", "--stage", "disposition", "--stage_n", "1", "--artifact", openPath],
|
|
162
|
+
env,
|
|
163
|
+
);
|
|
164
|
+
console.log(`exit=${open.code}`);
|
|
165
|
+
console.log((open.stderr || open.stdout).trim());
|
|
166
|
+
if (open.code !== 2 || !`${open.stderr}${open.stdout}`.includes("Join blocked")) {
|
|
167
|
+
console.error("FAIL: expected exit=2 and Join blocked for open conflicts");
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
console.log("fail_closed: ok");
|
|
171
|
+
|
|
172
|
+
// 4) Spoofed receipt must fail closed.
|
|
173
|
+
const spoofPath = path.join(dogfoodRoot, "spoof-disposition.json");
|
|
174
|
+
await fsp.writeFile(
|
|
175
|
+
spoofPath,
|
|
176
|
+
JSON.stringify({
|
|
177
|
+
schema: "ralplan.review_conflicts.v1",
|
|
178
|
+
plannerStageN: 1,
|
|
179
|
+
findings: [
|
|
180
|
+
{
|
|
181
|
+
...findings[0],
|
|
182
|
+
sourceReceipt: {
|
|
183
|
+
stage: "architect",
|
|
184
|
+
stageN: 1,
|
|
185
|
+
path: "/tmp/spoofed.md",
|
|
186
|
+
sha256: "deadbeef",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
findings[1],
|
|
190
|
+
],
|
|
191
|
+
dispositions: [
|
|
192
|
+
{
|
|
193
|
+
conflictId: "conflict:contract.field:arch-1:crit-1",
|
|
194
|
+
choice: "accept_architect",
|
|
195
|
+
rationale: "spoof should not land",
|
|
196
|
+
decisionOwner: "ralplan-leader",
|
|
197
|
+
affectedSections: ["## Contracts"],
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
console.log();
|
|
203
|
+
console.log("## 4) disposition with spoofed receipt (expect fail-closed)");
|
|
204
|
+
const spoof = await runGjc(
|
|
205
|
+
binary,
|
|
206
|
+
dogfoodRoot,
|
|
207
|
+
["ralplan", "--write", "--stage", "disposition", "--stage_n", "1", "--artifact", spoofPath],
|
|
208
|
+
env,
|
|
209
|
+
);
|
|
210
|
+
console.log(`exit=${spoof.code}`);
|
|
211
|
+
console.log((spoof.stderr || spoof.stdout).trim());
|
|
212
|
+
if (spoof.code !== 2 || !`${spoof.stderr}${spoof.stdout}`.includes("does not match indexed")) {
|
|
213
|
+
console.error("FAIL: expected exit=2 for spoofed receipt");
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
console.log("spoof_rejected: ok");
|
|
217
|
+
|
|
218
|
+
// 5) Complete disposition must accept and persist.
|
|
219
|
+
const closedPath = path.join(dogfoodRoot, "closed-disposition.json");
|
|
220
|
+
await fsp.writeFile(
|
|
221
|
+
closedPath,
|
|
222
|
+
JSON.stringify({
|
|
223
|
+
schema: "ralplan.review_conflicts.v1",
|
|
224
|
+
plannerStageN: 1,
|
|
225
|
+
findings,
|
|
226
|
+
dispositions: [
|
|
227
|
+
{
|
|
228
|
+
conflictId: "conflict:contract.field:arch-1:crit-1",
|
|
229
|
+
choice: "accept_architect",
|
|
230
|
+
rationale: "Field duplicates existing session identity.",
|
|
231
|
+
decisionOwner: "ralplan-leader",
|
|
232
|
+
affectedSections: ["## Contracts"],
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
console.log();
|
|
238
|
+
console.log("## 5) disposition stage with complete dispositions (expect accept)");
|
|
239
|
+
const closed = await runGjc(
|
|
240
|
+
binary,
|
|
241
|
+
dogfoodRoot,
|
|
242
|
+
["ralplan", "--write", "--stage", "disposition", "--stage_n", "1", "--artifact", closedPath, "--json"],
|
|
243
|
+
env,
|
|
244
|
+
);
|
|
245
|
+
console.log(`exit=${closed.code}`);
|
|
246
|
+
console.log(closed.stdout.trim() || closed.stderr.trim());
|
|
247
|
+
if (closed.code !== 0) {
|
|
248
|
+
console.error("FAIL: expected disposition write success");
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
const payload = JSON.parse(closed.stdout) as { path?: string; stage?: string };
|
|
252
|
+
if (payload.stage !== "disposition" || !payload.path) {
|
|
253
|
+
console.error("FAIL: unexpected write payload", payload);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
const body = await fsp.readFile(payload.path, "utf-8");
|
|
257
|
+
console.log();
|
|
258
|
+
console.log("## 6) persisted disposition artifact");
|
|
259
|
+
console.log(`path=${payload.path}`);
|
|
260
|
+
console.log(body.slice(0, 1200));
|
|
261
|
+
if (!body.includes("ralplan.review_conflicts.v1") || !body.includes("dispositioned")) {
|
|
262
|
+
console.error("FAIL: artifact missing schema or dispositioned status");
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
console.log();
|
|
267
|
+
console.log("DOGFOOD_OK");
|
|
268
|
+
console.log(`DOGFOOD_BINARY=${binary}`);
|
|
269
|
+
console.log(`DOGFOOD_HEAD=${full}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await main();
|
|
@@ -35,6 +35,8 @@ const LOCKED_EXCLUSIONS: Readonly<Record<string, string>> = {
|
|
|
35
35
|
"slash_command:transcript": "visual/local-only transcript viewer, not a user-facing SDK control seam",
|
|
36
36
|
"slash_command:sessions": "visual/local-only sessions dashboard, not a user-facing SDK control seam",
|
|
37
37
|
"agent_session:constructor": "internal accessor/plumbing, not a user-facing control seam",
|
|
38
|
+
"agent_session:registerToolSessionCleanup":
|
|
39
|
+
"internal tool lifecycle cleanup registration, not a user-facing SDK control seam",
|
|
38
40
|
"agent_session:nextToolChoice": "internal accessor/plumbing, not a user-facing control seam",
|
|
39
41
|
"agent_session:setForcedToolChoice": "internal accessor/plumbing, not a user-facing control seam",
|
|
40
42
|
"agent_session:getActiveSkillState": "internal accessor/plumbing, not a user-facing control seam",
|
|
@@ -225,6 +225,11 @@ async function checkoutAcpxSource(): Promise<AcpxCheckout> {
|
|
|
225
225
|
await commandOutput(["git", "clean", "-fdx", "--exclude=node_modules"], root);
|
|
226
226
|
const status = await commandOutput(["git", "status", "--porcelain"], root);
|
|
227
227
|
if (status.length > 0) throw new Error(`acpx source checkout is not clean at ${ACPX_GIT_HEAD}.`);
|
|
228
|
+
// The upstream runner resolves its own imports (`@agentclientprotocol/sdk`, `zod`)
|
|
229
|
+
// from this checkout, not from the gjc workspace, so the pin has to be installed
|
|
230
|
+
// before it can run. `git clean` deliberately preserves node_modules so a warm
|
|
231
|
+
// cache skips the reinstall.
|
|
232
|
+
await commandOutput(["bun", "install", "--no-save", "--ignore-scripts"], root);
|
|
228
233
|
return { root, head: await commandOutput(["git", "rev-parse", "HEAD"], root) };
|
|
229
234
|
}
|
|
230
235
|
|
package/src/async/job-manager.ts
CHANGED
|
@@ -525,9 +525,9 @@ export class AsyncJobManager {
|
|
|
525
525
|
|
|
526
526
|
if (job.status === "cancelled") {
|
|
527
527
|
job.resultText = outcome.kind === "paused" ? outcome.note : outcome.text;
|
|
528
|
+
this.#markRecordTerminal(id, "cancelled");
|
|
528
529
|
this.#runLifecycle(id, "terminal", job);
|
|
529
530
|
this.#scheduleEviction(id);
|
|
530
|
-
this.#markRecordTerminal(id, "cancelled");
|
|
531
531
|
this.#drainResumeQueue();
|
|
532
532
|
return;
|
|
533
533
|
}
|
|
@@ -539,6 +539,7 @@ export class AsyncJobManager {
|
|
|
539
539
|
this.#freezeEndTime(job);
|
|
540
540
|
if (outcome.note) job.resultText = outcome.note;
|
|
541
541
|
this.#markRecordPaused(id);
|
|
542
|
+
this.#notifyChange();
|
|
542
543
|
this.#drainResumeQueue();
|
|
543
544
|
return;
|
|
544
545
|
}
|
|
@@ -547,10 +548,10 @@ export class AsyncJobManager {
|
|
|
547
548
|
job.setupFailureSummary = outcome.setupFailureSummary;
|
|
548
549
|
this.#freezeEndTime(job);
|
|
549
550
|
job.errorText = outcome.text;
|
|
551
|
+
this.#markRecordTerminal(id, "failed");
|
|
550
552
|
this.#enqueueDelivery(id, outcome.text);
|
|
551
553
|
this.#runLifecycle(id, "terminal", job);
|
|
552
554
|
this.#scheduleEviction(id);
|
|
553
|
-
this.#markRecordTerminal(id, "failed");
|
|
554
555
|
this.#drainResumeQueue();
|
|
555
556
|
return;
|
|
556
557
|
}
|
|
@@ -558,28 +559,28 @@ export class AsyncJobManager {
|
|
|
558
559
|
job.status = "completed";
|
|
559
560
|
this.#freezeEndTime(job);
|
|
560
561
|
job.resultText = outcome.text;
|
|
562
|
+
this.#markRecordTerminal(id, "completed");
|
|
561
563
|
this.#enqueueDelivery(id, outcome.text);
|
|
562
564
|
this.#runLifecycle(id, "terminal", job);
|
|
563
565
|
this.#scheduleEviction(id);
|
|
564
|
-
this.#markRecordTerminal(id, "completed");
|
|
565
566
|
this.#drainResumeQueue();
|
|
566
567
|
} catch (error) {
|
|
567
568
|
if (job.status === "cancelled") {
|
|
568
569
|
job.errorText = error instanceof Error ? error.message : String(error);
|
|
570
|
+
this.#markRecordTerminal(id, "cancelled");
|
|
569
571
|
this.#runLifecycle(id, "terminal", job);
|
|
570
572
|
this.#scheduleEviction(id);
|
|
571
|
-
this.#markRecordTerminal(id, "cancelled");
|
|
572
573
|
this.#drainResumeQueue();
|
|
573
574
|
return;
|
|
574
575
|
}
|
|
575
|
-
this.#runLifecycle(id, "terminal", job);
|
|
576
576
|
const errorText = error instanceof Error ? error.message : String(error);
|
|
577
577
|
job.status = "failed";
|
|
578
578
|
this.#freezeEndTime(job);
|
|
579
579
|
job.errorText = errorText;
|
|
580
|
+
this.#markRecordTerminal(id, "failed");
|
|
580
581
|
this.#enqueueDelivery(id, errorText);
|
|
582
|
+
this.#runLifecycle(id, "terminal", job);
|
|
581
583
|
this.#scheduleEviction(id);
|
|
582
|
-
this.#markRecordTerminal(id, "failed");
|
|
583
584
|
this.#drainResumeQueue();
|
|
584
585
|
}
|
|
585
586
|
})();
|
|
@@ -599,20 +600,22 @@ export class AsyncJobManager {
|
|
|
599
600
|
if (!job) return false;
|
|
600
601
|
if (filter?.ownerId && job.ownerId !== filter.ownerId) return false;
|
|
601
602
|
if (job.status === "paused") {
|
|
602
|
-
this.#runLifecycle(id, "cancel");
|
|
603
603
|
// Paused jobs have no running promise to abort; transition directly.
|
|
604
604
|
// The session file is kept, so the record stays resumable by id.
|
|
605
605
|
job.status = "cancelled";
|
|
606
606
|
this.#markRecordTerminal(id, "cancelled");
|
|
607
|
+
this.#runLifecycle(id, "cancel");
|
|
607
608
|
this.#scheduleEviction(id);
|
|
608
609
|
this.#drainResumeQueue();
|
|
609
610
|
return true;
|
|
610
611
|
}
|
|
611
612
|
if (job.status !== "running") return false;
|
|
612
|
-
this.#runLifecycle(id, "cancel");
|
|
613
613
|
job.status = "cancelled";
|
|
614
614
|
this.#freezeEndTime(job);
|
|
615
|
+
this.#markRecordTerminal(id, "cancelled");
|
|
616
|
+
this.#runLifecycle(id, "cancel");
|
|
615
617
|
job.abortController.abort();
|
|
618
|
+
this.#notifyChange();
|
|
616
619
|
return true;
|
|
617
620
|
}
|
|
618
621
|
|
|
@@ -696,6 +699,7 @@ export class AsyncJobManager {
|
|
|
696
699
|
/** Register or replace the canonical record for a subagent. */
|
|
697
700
|
registerSubagentRecord(record: SubagentRecord): void {
|
|
698
701
|
this.#subagentRecords.set(record.subagentId, record);
|
|
702
|
+
this.#notifyChange();
|
|
699
703
|
}
|
|
700
704
|
|
|
701
705
|
/** Patch model metadata onto an existing subagent record (best-effort; no-op if unknown). */
|
|
@@ -1136,6 +1140,7 @@ export class AsyncJobManager {
|
|
|
1136
1140
|
message,
|
|
1137
1141
|
createdAt: rec.queued.createdAt,
|
|
1138
1142
|
});
|
|
1143
|
+
this.#notifyChange();
|
|
1139
1144
|
return { ok: true, queued: true, status: "queued" };
|
|
1140
1145
|
}
|
|
1141
1146
|
return this.#startResume(rec, message, descriptor);
|
|
@@ -1160,6 +1165,7 @@ export class AsyncJobManager {
|
|
|
1160
1165
|
rec.currentJobId = newJobId;
|
|
1161
1166
|
rec.status = this.#jobs.get(newJobId)?.status ?? "running";
|
|
1162
1167
|
rec.queued = undefined;
|
|
1168
|
+
this.#notifyChange();
|
|
1163
1169
|
return { ok: true, status: rec.status, jobId: newJobId };
|
|
1164
1170
|
}
|
|
1165
1171
|
|
|
@@ -1202,16 +1208,15 @@ export class AsyncJobManager {
|
|
|
1202
1208
|
if (!rec) return false;
|
|
1203
1209
|
if (rec.status === "running" && rec.currentJobId) return this.cancel(rec.currentJobId, filter);
|
|
1204
1210
|
if (rec.status === "paused") {
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
this.#scheduleEviction(rec.currentJobId);
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1211
|
+
const currentJobId = rec.currentJobId;
|
|
1212
|
+
const job = currentJobId ? this.#jobs.get(currentJobId) : undefined;
|
|
1213
|
+
const shouldScheduleEviction = job?.status === "paused";
|
|
1214
|
+
if (shouldScheduleEviction) job.status = "cancelled";
|
|
1212
1215
|
rec.status = "cancelled";
|
|
1213
1216
|
this.#liveHandles.delete(rec.subagentId);
|
|
1214
1217
|
this.#subagentProgress.delete(rec.subagentId);
|
|
1218
|
+
if (shouldScheduleEviction && currentJobId) this.#scheduleEviction(currentJobId);
|
|
1219
|
+
else this.#notifyChange();
|
|
1215
1220
|
this.#drainResumeQueue();
|
|
1216
1221
|
return true;
|
|
1217
1222
|
}
|
|
@@ -1221,6 +1226,7 @@ export class AsyncJobManager {
|
|
|
1221
1226
|
rec.status = "cancelled";
|
|
1222
1227
|
rec.queued = undefined;
|
|
1223
1228
|
this.#subagentProgress.delete(rec.subagentId);
|
|
1229
|
+
this.#notifyChange();
|
|
1224
1230
|
return true;
|
|
1225
1231
|
}
|
|
1226
1232
|
return false;
|
|
@@ -1471,11 +1477,7 @@ export class AsyncJobManager {
|
|
|
1471
1477
|
*/
|
|
1472
1478
|
cancelAll(filter?: AsyncJobFilter): void {
|
|
1473
1479
|
for (const job of this.getRunningJobs(filter)) {
|
|
1474
|
-
this
|
|
1475
|
-
job.status = "cancelled";
|
|
1476
|
-
this.#freezeEndTime(job);
|
|
1477
|
-
job.abortController.abort();
|
|
1478
|
-
this.#scheduleEviction(job.id);
|
|
1480
|
+
if (this.cancel(job.id, filter)) this.#scheduleEviction(job.id);
|
|
1479
1481
|
}
|
|
1480
1482
|
}
|
|
1481
1483
|
|
|
@@ -873,8 +873,8 @@ export const SETTINGS_SCHEMA = {
|
|
|
873
873
|
default: true,
|
|
874
874
|
ui: {
|
|
875
875
|
tab: "appearance",
|
|
876
|
-
label: "
|
|
877
|
-
description: "Show contextual keyboard shortcuts in the
|
|
876
|
+
label: "Composer Shortcut Hints",
|
|
877
|
+
description: "Show contextual keyboard shortcuts in the composer placeholder",
|
|
878
878
|
},
|
|
879
879
|
},
|
|
880
880
|
|
package/src/debug/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { getSessionsDir } from "@gajae-code/utils";
|
|
|
11
11
|
import { DynamicBorder } from "../modes/components/dynamic-border";
|
|
12
12
|
import { getSelectListTheme, getSymbolTheme, theme } from "../modes/theme/theme";
|
|
13
13
|
import type { InteractiveModeContext } from "../modes/types";
|
|
14
|
+
import { suspendInteractiveActivityIndicator } from "../modes/types";
|
|
14
15
|
import { formatBytes } from "../tools/render-utils";
|
|
15
16
|
import { openPath } from "../utils/open";
|
|
16
17
|
import { DebugLogViewerComponent } from "./log-viewer";
|
|
@@ -77,10 +78,12 @@ export class DebugSelectorComponent extends Container {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
handleInput(keyData: string): void {
|
|
81
|
+
if (this.ctx.isStopped?.()) return;
|
|
80
82
|
this.#selectList.handleInput(keyData);
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
async #handleSelection(value: string): Promise<void> {
|
|
86
|
+
if (this.ctx.isStopped?.()) return;
|
|
84
87
|
switch (value) {
|
|
85
88
|
case "open-artifacts":
|
|
86
89
|
await this.#handleOpenArtifacts();
|
|
@@ -115,12 +118,23 @@ export class DebugSelectorComponent extends Container {
|
|
|
115
118
|
}
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
#finishStatusLoader(loader: Loader, releaseActivityIndicator: () => void): void {
|
|
122
|
+
loader.stop();
|
|
123
|
+
if (!this.ctx.isStopped?.()) this.ctx.statusContainer.clear();
|
|
124
|
+
releaseActivityIndicator();
|
|
125
|
+
}
|
|
126
|
+
|
|
118
127
|
async #handlePerformanceReport(): Promise<void> {
|
|
119
128
|
// Start profiling
|
|
120
129
|
let session: ProfilerSession;
|
|
121
130
|
try {
|
|
122
131
|
session = await startCpuProfile();
|
|
132
|
+
if (this.ctx.isStopped?.()) {
|
|
133
|
+
await session.stop().catch(() => {});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
123
136
|
} catch (err) {
|
|
137
|
+
if (this.ctx.isStopped?.()) return;
|
|
124
138
|
this.ctx.showError(`Failed to start profiler: ${err instanceof Error ? err.message : String(err)}`);
|
|
125
139
|
return;
|
|
126
140
|
}
|
|
@@ -151,9 +165,17 @@ export class DebugSelectorComponent extends Container {
|
|
|
151
165
|
resolve();
|
|
152
166
|
};
|
|
153
167
|
|
|
154
|
-
|
|
168
|
+
const stopped = Promise.withResolvers<void>();
|
|
169
|
+
const unsubscribeStop = this.ctx.onStop?.(() => stopped.resolve());
|
|
170
|
+
const stopWon = await Promise.race([promise.then(() => false), stopped.promise.then(() => true)]);
|
|
171
|
+
unsubscribeStop?.();
|
|
172
|
+
if (stopWon || this.ctx.isStopped?.()) {
|
|
173
|
+
await session.stop().catch(() => {});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
155
176
|
|
|
156
177
|
// Stop profiling and create report
|
|
178
|
+
const releaseActivityIndicator = suspendInteractiveActivityIndicator(this.ctx);
|
|
157
179
|
const loader = new Loader(
|
|
158
180
|
this.ctx.ui,
|
|
159
181
|
spinner => theme.fg("accent", spinner),
|
|
@@ -174,8 +196,8 @@ export class DebugSelectorComponent extends Container {
|
|
|
174
196
|
workProfile,
|
|
175
197
|
});
|
|
176
198
|
|
|
177
|
-
loader
|
|
178
|
-
this.ctx.
|
|
199
|
+
this.#finishStatusLoader(loader, releaseActivityIndicator);
|
|
200
|
+
if (this.ctx.isStopped?.()) return;
|
|
179
201
|
|
|
180
202
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
181
203
|
this.ctx.chatContainer.addChild(
|
|
@@ -184,8 +206,8 @@ export class DebugSelectorComponent extends Container {
|
|
|
184
206
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
|
|
185
207
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
|
|
186
208
|
} catch (err) {
|
|
187
|
-
loader
|
|
188
|
-
this.ctx.
|
|
209
|
+
this.#finishStatusLoader(loader, releaseActivityIndicator);
|
|
210
|
+
if (this.ctx.isStopped?.()) return;
|
|
189
211
|
this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
|
|
190
212
|
}
|
|
191
213
|
|
|
@@ -219,6 +241,7 @@ export class DebugSelectorComponent extends Container {
|
|
|
219
241
|
}
|
|
220
242
|
|
|
221
243
|
async #handleDumpReport(): Promise<void> {
|
|
244
|
+
const releaseActivityIndicator = suspendInteractiveActivityIndicator(this.ctx);
|
|
222
245
|
const loader = new Loader(
|
|
223
246
|
this.ctx.ui,
|
|
224
247
|
spinner => theme.fg("accent", spinner),
|
|
@@ -235,8 +258,8 @@ export class DebugSelectorComponent extends Container {
|
|
|
235
258
|
settings: this.#getResolvedSettings(),
|
|
236
259
|
});
|
|
237
260
|
|
|
238
|
-
loader
|
|
239
|
-
this.ctx.
|
|
261
|
+
this.#finishStatusLoader(loader, releaseActivityIndicator);
|
|
262
|
+
if (this.ctx.isStopped?.()) return;
|
|
240
263
|
|
|
241
264
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
242
265
|
this.ctx.chatContainer.addChild(
|
|
@@ -245,8 +268,8 @@ export class DebugSelectorComponent extends Container {
|
|
|
245
268
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
|
|
246
269
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
|
|
247
270
|
} catch (err) {
|
|
248
|
-
loader
|
|
249
|
-
this.ctx.
|
|
271
|
+
this.#finishStatusLoader(loader, releaseActivityIndicator);
|
|
272
|
+
if (this.ctx.isStopped?.()) return;
|
|
250
273
|
this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
|
|
251
274
|
}
|
|
252
275
|
|
|
@@ -254,6 +277,7 @@ export class DebugSelectorComponent extends Container {
|
|
|
254
277
|
}
|
|
255
278
|
|
|
256
279
|
async #handleMemoryReport(): Promise<void> {
|
|
280
|
+
const releaseActivityIndicator = suspendInteractiveActivityIndicator(this.ctx);
|
|
257
281
|
const loader = new Loader(
|
|
258
282
|
this.ctx.ui,
|
|
259
283
|
spinner => theme.fg("accent", spinner),
|
|
@@ -274,8 +298,8 @@ export class DebugSelectorComponent extends Container {
|
|
|
274
298
|
heapSnapshot,
|
|
275
299
|
});
|
|
276
300
|
|
|
277
|
-
loader
|
|
278
|
-
this.ctx.
|
|
301
|
+
this.#finishStatusLoader(loader, releaseActivityIndicator);
|
|
302
|
+
if (this.ctx.isStopped?.()) return;
|
|
279
303
|
|
|
280
304
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
281
305
|
this.ctx.chatContainer.addChild(
|
|
@@ -284,8 +308,8 @@ export class DebugSelectorComponent extends Container {
|
|
|
284
308
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
|
|
285
309
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
|
|
286
310
|
} catch (err) {
|
|
287
|
-
loader
|
|
288
|
-
this.ctx.
|
|
311
|
+
this.#finishStatusLoader(loader, releaseActivityIndicator);
|
|
312
|
+
if (this.ctx.isStopped?.()) return;
|
|
289
313
|
this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
|
|
290
314
|
}
|
|
291
315
|
|
|
@@ -405,6 +429,7 @@ export class DebugSelectorComponent extends Container {
|
|
|
405
429
|
}
|
|
406
430
|
|
|
407
431
|
// Clear cache
|
|
432
|
+
const releaseActivityIndicator = suspendInteractiveActivityIndicator(this.ctx);
|
|
408
433
|
const loader = new Loader(
|
|
409
434
|
this.ctx.ui,
|
|
410
435
|
spinner => theme.fg("accent", spinner),
|
|
@@ -418,8 +443,8 @@ export class DebugSelectorComponent extends Container {
|
|
|
418
443
|
try {
|
|
419
444
|
const result = await clearArtifactCache(sessionsDir, 30);
|
|
420
445
|
|
|
421
|
-
loader
|
|
422
|
-
this.ctx.
|
|
446
|
+
this.#finishStatusLoader(loader, releaseActivityIndicator);
|
|
447
|
+
if (this.ctx.isStopped?.()) return;
|
|
423
448
|
|
|
424
449
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
425
450
|
this.ctx.chatContainer.addChild(
|
|
@@ -430,8 +455,8 @@ export class DebugSelectorComponent extends Container {
|
|
|
430
455
|
),
|
|
431
456
|
);
|
|
432
457
|
} catch (err) {
|
|
433
|
-
loader
|
|
434
|
-
this.ctx.
|
|
458
|
+
this.#finishStatusLoader(loader, releaseActivityIndicator);
|
|
459
|
+
if (this.ctx.isStopped?.()) return;
|
|
435
460
|
this.ctx.showError(`Failed to clear cache: ${err instanceof Error ? err.message : String(err)}`);
|
|
436
461
|
}
|
|
437
462
|
|