pikiloom 0.4.42 → 0.4.44
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/dashboard/dist/assets/{AgentTab-B9z4W2mh.js → AgentTab-B8V2eV_D.js} +1 -1
- package/dashboard/dist/assets/{ConnectionModal-2bL7sFKk.js → ConnectionModal-BOVwXLJB.js} +1 -1
- package/dashboard/dist/assets/{DirBrowser-CA4k8E_A.js → DirBrowser-DbeAWYiL.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-CRjTmFRl.js → ExtensionsTab-BMS3PW9N.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-DDb7rsHe.js → IMAccessTab-B1sRgJk3.js} +1 -1
- package/dashboard/dist/assets/{Modal-DDiBG4kb.js → Modal-DCwGz46r.js} +1 -1
- package/dashboard/dist/assets/{Modals-CsUQjpxn.js → Modals-Bh_0en5P.js} +1 -1
- package/dashboard/dist/assets/{Select-CG_7h_Tz.js → Select-DC6zguGC.js} +1 -1
- package/dashboard/dist/assets/{SessionPanel-B5964GUj.js → SessionPanel-DNlLd-PL.js} +1 -1
- package/dashboard/dist/assets/{SystemTab-D7r1PsC9.js → SystemTab-uFqZinIZ.js} +1 -1
- package/dashboard/dist/assets/{index-BbplxgnQ.js → index-CL5H13Cl.js} +2 -2
- package/dashboard/dist/assets/index-DooLxbPX.js +23 -0
- package/dashboard/dist/assets/{shared-Bv8WvQSo.js → shared-Byy2BNLq.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dist/agent/accounts.js +2 -2
- package/dist/agent/drivers/claude.js +84 -38
- package/dist/agent/drivers/codex.js +30 -2
- package/dist/agent/kernel-bridge.js +27 -3
- package/dist/bot/commands.js +43 -4
- package/dist/bot/render-shared.js +29 -23
- package/dist/channels/dingtalk/bot.js +3 -0
- package/dist/channels/discord/bot.js +3 -0
- package/dist/channels/feishu/render.js +3 -7
- package/dist/channels/slack/bot.js +3 -0
- package/dist/channels/telegram/bot.js +2 -2
- package/dist/channels/telegram/render.js +3 -3
- package/dist/channels/wecom/bot.js +3 -0
- package/dist/channels/weixin/bot.js +3 -0
- package/dist/dashboard/routes/accounts.js +7 -0
- package/dist/dashboard/routes/agents.js +1 -1
- package/package.json +1 -1
- package/packages/kernel/dist/drivers/claude.d.ts +12 -1
- package/packages/kernel/dist/drivers/claude.js +243 -26
- package/packages/kernel/dist/drivers/codex.js +15 -3
- package/packages/kernel/dist/index.d.ts +1 -1
- package/packages/kernel/dist/index.js +1 -1
- package/dashboard/dist/assets/index-CULTTvtY.js +0 -23
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { extname } from 'node:path';
|
|
2
4
|
import { discoverClaudeNativeSessions } from '../workspace/native.js';
|
|
3
5
|
// Real driver: shells the local `claude` CLI in stream-json mode and normalizes its
|
|
4
6
|
// events into kernel DriverEvents. Faithful to pikiloom's claude.ts event shapes
|
|
@@ -28,7 +30,14 @@ export class ClaudeDriver {
|
|
|
28
30
|
}
|
|
29
31
|
run(input, ctx) {
|
|
30
32
|
const steerable = !!input.steerable;
|
|
31
|
-
|
|
33
|
+
// Always drive Claude over a stream-json stdin and keep that stdin OPEN for the whole turn.
|
|
34
|
+
// That open stdin is what lets Claude's native "launch detached background work → end the
|
|
35
|
+
// turn → wake itself up and report when the work finishes" flow play out: it only happens
|
|
36
|
+
// while stdin stays open (with it closed, Claude exits at the first `result` and the wake-up
|
|
37
|
+
// — and any in-process background agent/workflow — is lost). Image attachments also need the
|
|
38
|
+
// stream-json user message (a plain text stdin can't carry images). `--replay-user-messages`
|
|
39
|
+
// + registerSteer remain the only mid-turn-steer-specific bits.
|
|
40
|
+
const args = ['-p', '--verbose', '--output-format', 'stream-json', '--include-partial-messages', '--input-format', 'stream-json'];
|
|
32
41
|
if (input.model)
|
|
33
42
|
args.push('--model', input.model);
|
|
34
43
|
if (input.effort)
|
|
@@ -42,7 +51,7 @@ export class ClaudeDriver {
|
|
|
42
51
|
if (input.permissionMode)
|
|
43
52
|
args.push('--permission-mode', input.permissionMode); // parity: keep bypass/accept-edits on the kernel path
|
|
44
53
|
if (steerable)
|
|
45
|
-
args.push('--
|
|
54
|
+
args.push('--replay-user-messages'); // parity: mid-turn steer
|
|
46
55
|
if (input.extraArgs?.length)
|
|
47
56
|
args.push(...input.extraArgs);
|
|
48
57
|
const state = {
|
|
@@ -54,26 +63,57 @@ export class ClaudeDriver {
|
|
|
54
63
|
contextWindow: null, turnOutputTokensBase: 0,
|
|
55
64
|
subAgents: new Map(),
|
|
56
65
|
tools: new Map(),
|
|
66
|
+
taskList: new Map(),
|
|
67
|
+
taskOrder: [],
|
|
68
|
+
pendingTaskCreates: new Map(),
|
|
69
|
+
// run_in_background lifecycle: task ids seen as started vs. reached a terminal status.
|
|
70
|
+
bgStarted: new Set(), bgTerminal: new Set(),
|
|
57
71
|
};
|
|
58
72
|
return new Promise((resolve) => {
|
|
59
73
|
let child;
|
|
60
74
|
let settled = false;
|
|
61
|
-
|
|
75
|
+
let holdCapTimer = null;
|
|
76
|
+
const usageOf = () => this.usage(state);
|
|
77
|
+
const clearHoldCap = () => { if (holdCapTimer) {
|
|
78
|
+
clearTimeout(holdCapTimer);
|
|
79
|
+
holdCapTimer = null;
|
|
80
|
+
} };
|
|
81
|
+
// kill=true SIGTERMs immediately — fast exit, used once nothing is left running in the
|
|
82
|
+
// background (a normal turn, or a wake-up turn after every background task finished).
|
|
83
|
+
// kill=false only ends stdin and lets Claude shut down on its own, so any still-running
|
|
84
|
+
// detached background work survives a clean exit (a hard kill mid-flight is exactly what
|
|
85
|
+
// tore the background — and the wake-up — down before). A leak-guard SIGTERM is the backstop.
|
|
86
|
+
const finish = (r, kill = true) => {
|
|
62
87
|
if (settled)
|
|
63
88
|
return;
|
|
64
89
|
settled = true;
|
|
90
|
+
clearHoldCap();
|
|
65
91
|
try {
|
|
66
92
|
child?.stdin?.end();
|
|
67
93
|
}
|
|
68
94
|
catch { /* ignore */ }
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
95
|
+
if (!child.killed && child.exitCode == null) {
|
|
96
|
+
if (kill) {
|
|
97
|
+
try {
|
|
98
|
+
child.kill('SIGTERM');
|
|
99
|
+
}
|
|
100
|
+
catch { /* ignore */ }
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const guard = setTimeout(() => { try {
|
|
104
|
+
child?.kill('SIGTERM');
|
|
105
|
+
}
|
|
106
|
+
catch { /* ignore */ } }, CLAUDE_EXIT_LEAK_GUARD_MS);
|
|
107
|
+
if (typeof guard.unref === 'function')
|
|
108
|
+
guard.unref();
|
|
72
109
|
}
|
|
73
|
-
|
|
74
|
-
} // replay mode keeps the process alive past the turn
|
|
110
|
+
}
|
|
75
111
|
resolve(r);
|
|
76
112
|
};
|
|
113
|
+
const settleResult = (opts = {}) => finish({
|
|
114
|
+
ok: !state.error, text: state.text, reasoning: state.reasoning || undefined,
|
|
115
|
+
error: state.error, stopReason: opts.stopReason ?? state.stopReason, sessionId: state.sessionId, usage: usageOf(),
|
|
116
|
+
}, opts.kill ?? true);
|
|
77
117
|
try {
|
|
78
118
|
child = spawn(this.bin, args, { cwd: input.workdir, env: { ...process.env, ...(input.env || {}) }, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
79
119
|
}
|
|
@@ -90,9 +130,9 @@ export class ClaudeDriver {
|
|
|
90
130
|
else
|
|
91
131
|
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
92
132
|
if (steerable) {
|
|
93
|
-
ctx.registerSteer(async (prompt) => {
|
|
133
|
+
ctx.registerSteer(async (prompt, attachments) => {
|
|
94
134
|
try {
|
|
95
|
-
child.stdin.write(claudeUserMessage(prompt) + '\n');
|
|
135
|
+
child.stdin.write(claudeUserMessage(prompt, attachments) + '\n');
|
|
96
136
|
return true;
|
|
97
137
|
}
|
|
98
138
|
catch {
|
|
@@ -100,14 +140,17 @@ export class ClaudeDriver {
|
|
|
100
140
|
}
|
|
101
141
|
});
|
|
102
142
|
}
|
|
103
|
-
const usageOf = () => this.usage(state);
|
|
104
143
|
let buf = '';
|
|
105
144
|
let stderr = '';
|
|
106
145
|
child.stdout.on('data', (chunk) => {
|
|
146
|
+
if (settled)
|
|
147
|
+
return; // ignore the process's post-settle shutdown chatter
|
|
107
148
|
buf += chunk.toString('utf8');
|
|
108
149
|
const lines = buf.split('\n');
|
|
109
150
|
buf = lines.pop() || '';
|
|
110
151
|
for (const line of lines) {
|
|
152
|
+
if (settled)
|
|
153
|
+
return;
|
|
111
154
|
const trimmed = line.trim();
|
|
112
155
|
if (!trimmed)
|
|
113
156
|
continue;
|
|
@@ -119,29 +162,42 @@ export class ClaudeDriver {
|
|
|
119
162
|
continue;
|
|
120
163
|
}
|
|
121
164
|
handleClaudeEvent(ev, state, ctx.emit);
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
165
|
+
if (ev.type !== 'result')
|
|
166
|
+
continue;
|
|
167
|
+
const hasError = !!ev.is_error || (Array.isArray(ev.errors) && ev.errors.length > 0) || !!state.error;
|
|
168
|
+
const pending = pendingClaudeBackgroundTasks(state);
|
|
169
|
+
if (decideClaudeResultSettle({ hasError, pendingBackground: pending }) === 'hold') {
|
|
170
|
+
// Background work is still running: do NOT end the turn here. Keep stdin open so Claude
|
|
171
|
+
// wakes itself up and streams a follow-up turn reporting the finished work — we settle
|
|
172
|
+
// on THAT result (pending back to 0). A cap bounds the wait so a never-completing daemon
|
|
173
|
+
// (e.g. a dev server) eventually settles (graceful, no kill) instead of holding forever.
|
|
174
|
+
if (!holdCapTimer) {
|
|
175
|
+
holdCapTimer = setTimeout(() => { if (!settled)
|
|
176
|
+
settleResult({ stopReason: 'background', kill: false }); }, claudeBgHoldCapMs());
|
|
177
|
+
if (typeof holdCapTimer.unref === 'function')
|
|
178
|
+
holdCapTimer.unref();
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
125
181
|
}
|
|
182
|
+
settleResult();
|
|
126
183
|
}
|
|
127
184
|
});
|
|
128
185
|
child.stderr.on('data', (c) => { stderr += c.toString('utf8'); });
|
|
129
186
|
child.on('error', (err) => finish({ ok: false, text: state.text, error: `claude spawn error: ${err.message}`, stopReason: 'error' }));
|
|
130
187
|
child.on('close', (code) => {
|
|
188
|
+
if (settled)
|
|
189
|
+
return;
|
|
131
190
|
if (ctx.signal.aborted) {
|
|
132
|
-
finish({ ok: false, text: state.text, reasoning: state.reasoning, error: 'Interrupted by user.', stopReason: 'interrupted', sessionId: state.sessionId, usage: usageOf() });
|
|
191
|
+
finish({ ok: false, text: state.text, reasoning: state.reasoning, error: 'Interrupted by user.', stopReason: 'interrupted', sessionId: state.sessionId, usage: usageOf() }, false);
|
|
133
192
|
return;
|
|
134
193
|
}
|
|
135
194
|
const ok = !state.error && code === 0;
|
|
136
|
-
finish({ ok, text: state.text, reasoning: state.reasoning || undefined, error: state.error || (ok ? null : `claude exited ${code}${stderr ? `: ${stderr.slice(0, 300)}` : ''}`), stopReason: state.stopReason, sessionId: state.sessionId, usage: usageOf() });
|
|
195
|
+
finish({ ok, text: state.text, reasoning: state.reasoning || undefined, error: state.error || (ok ? null : `claude exited ${code}${stderr ? `: ${stderr.slice(0, 300)}` : ''}`), stopReason: state.stopReason, sessionId: state.sessionId, usage: usageOf() }, false);
|
|
137
196
|
});
|
|
138
197
|
try {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
child.stdin.write(input.prompt);
|
|
143
|
-
child.stdin.end();
|
|
144
|
-
}
|
|
198
|
+
// Send the prompt as a stream-json user message and keep stdin OPEN (do not end it here):
|
|
199
|
+
// closing it makes Claude exit at the first `result`, before any background task finishes.
|
|
200
|
+
child.stdin.write(claudeUserMessage(input.prompt, input.attachments) + '\n');
|
|
145
201
|
}
|
|
146
202
|
catch { /* ignore */ }
|
|
147
203
|
});
|
|
@@ -215,6 +271,7 @@ export function handleClaudeEvent(ev, s, emit) {
|
|
|
215
271
|
return;
|
|
216
272
|
}
|
|
217
273
|
if (t === 'system') {
|
|
274
|
+
trackClaudeBackgroundTask(ev, s);
|
|
218
275
|
if (ev.session_id && ev.session_id !== s.sessionId) {
|
|
219
276
|
s.sessionId = ev.session_id;
|
|
220
277
|
emit({ type: 'session', sessionId: ev.session_id });
|
|
@@ -301,6 +358,34 @@ export function handleClaudeEvent(ev, s, emit) {
|
|
|
301
358
|
emit({ type: 'plan', plan });
|
|
302
359
|
continue;
|
|
303
360
|
}
|
|
361
|
+
// Task list (current Claude mechanism): stash the subject; the tool_result assigns the id.
|
|
362
|
+
// Plan-only — like the legacy driver these never surface as Activity rows.
|
|
363
|
+
if (name === 'TaskCreate') {
|
|
364
|
+
const subject = typeof b.input?.subject === 'string' ? b.input.subject.trim() : '';
|
|
365
|
+
if (subject)
|
|
366
|
+
(s.pendingTaskCreates ||= new Map()).set(id, { subject });
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (name === 'TaskUpdate') {
|
|
370
|
+
const taskId = String(b.input?.taskId ?? '').trim();
|
|
371
|
+
const rawStatus = String(b.input?.status ?? '').trim().toLowerCase();
|
|
372
|
+
if (taskId) {
|
|
373
|
+
if (rawStatus === 'deleted') {
|
|
374
|
+
s.taskList?.delete(taskId);
|
|
375
|
+
if (Array.isArray(s.taskOrder))
|
|
376
|
+
s.taskOrder = s.taskOrder.filter((x) => x !== taskId);
|
|
377
|
+
}
|
|
378
|
+
else if (rawStatus) {
|
|
379
|
+
const existing = s.taskList?.get(taskId);
|
|
380
|
+
if (existing)
|
|
381
|
+
existing.status = rawStatus;
|
|
382
|
+
}
|
|
383
|
+
const plan = rebuildClaudeTaskPlan(s);
|
|
384
|
+
if (plan)
|
|
385
|
+
emit({ type: 'plan', plan });
|
|
386
|
+
}
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
304
389
|
if (name === 'Task' || name === 'Agent') {
|
|
305
390
|
const input = b.input || {};
|
|
306
391
|
const sub = {
|
|
@@ -348,6 +433,23 @@ export function handleClaudeEvent(ev, s, emit) {
|
|
|
348
433
|
continue;
|
|
349
434
|
emitClaudeImages(b.content || [], s, emit);
|
|
350
435
|
const id = String(b.tool_use_id || '').trim();
|
|
436
|
+
// TaskCreate result: the assigned task id arrives here; register the task and emit the plan.
|
|
437
|
+
if (id && s.pendingTaskCreates?.has(id)) {
|
|
438
|
+
const pending = s.pendingTaskCreates.get(id);
|
|
439
|
+
const assignedId = readClaudeTaskCreateId(ev, b);
|
|
440
|
+
if (pending && assignedId) {
|
|
441
|
+
s.pendingTaskCreates.delete(id);
|
|
442
|
+
(s.taskList ||= new Map());
|
|
443
|
+
(s.taskOrder ||= []);
|
|
444
|
+
if (!s.taskList.has(assignedId))
|
|
445
|
+
s.taskOrder.push(assignedId);
|
|
446
|
+
s.taskList.set(assignedId, { subject: pending.subject, status: 'pending' });
|
|
447
|
+
const plan = rebuildClaudeTaskPlan(s);
|
|
448
|
+
if (plan)
|
|
449
|
+
emit({ type: 'plan', plan });
|
|
450
|
+
}
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
351
453
|
const tool = id ? s.tools?.get(id) : undefined;
|
|
352
454
|
if (!tool)
|
|
353
455
|
continue;
|
|
@@ -384,10 +486,86 @@ export function handleClaudeEvent(ev, s, emit) {
|
|
|
384
486
|
return;
|
|
385
487
|
}
|
|
386
488
|
}
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
489
|
+
// ── run_in_background lifecycle (background → wake-up) ───────────────────────────────────
|
|
490
|
+
// Claude streams a structured lifecycle for detached background work as `system` sub-events:
|
|
491
|
+
// { subtype:'task_started', task_id, tool_use_id, description } ← launched
|
|
492
|
+
// { subtype:'task_updated', task_id, patch:{ status } } ← progress / terminal
|
|
493
|
+
// { subtype:'task_notification', task_id, status } ← terminal (completed/killed/…)
|
|
494
|
+
// A task counts as pending from task_started until a terminal task_updated/task_notification.
|
|
495
|
+
// While any are pending the driver keeps the turn alive instead of ending it at `result`, so
|
|
496
|
+
// Claude's own background→wake-up turn (which reports the finished work) can stream in. Pure +
|
|
497
|
+
// exported for hermetic testing.
|
|
498
|
+
// How long, with no settle, to keep holding a turn open for a background task to finish and
|
|
499
|
+
// trigger Claude's wake-up turn, before giving up (so a never-completing daemon doesn't hang
|
|
500
|
+
// the turn forever). Override with PIKILOOM_CLAUDE_BG_HOLD_MS.
|
|
501
|
+
const CLAUDE_BG_HOLD_CAP_DEFAULT_MS = 10 * 60_000;
|
|
502
|
+
export function claudeBgHoldCapMs() {
|
|
503
|
+
const raw = Number(process.env.PIKILOOM_CLAUDE_BG_HOLD_MS);
|
|
504
|
+
return Number.isFinite(raw) && raw > 0 ? raw : CLAUDE_BG_HOLD_CAP_DEFAULT_MS;
|
|
505
|
+
}
|
|
506
|
+
// After we settle a held turn gracefully (stdin closed, no kill), force-kill the lingering
|
|
507
|
+
// process only if it hasn't exited on its own within this window. Backstop against leaks.
|
|
508
|
+
const CLAUDE_EXIT_LEAK_GUARD_MS = 15_000;
|
|
509
|
+
export function isTerminalTaskStatus(status) {
|
|
510
|
+
return /^(complete|done|success|succeed|finish|kill|fail|error|stop|cancel|abort|timed?_?out|timeout)/i
|
|
511
|
+
.test(String(status ?? '').trim());
|
|
512
|
+
}
|
|
513
|
+
export function trackClaudeBackgroundTask(ev, s) {
|
|
514
|
+
const subtype = ev?.subtype;
|
|
515
|
+
if (subtype !== 'task_started' && subtype !== 'task_updated' && subtype !== 'task_notification')
|
|
516
|
+
return;
|
|
517
|
+
const id = String(ev?.task_id ?? ev?.tool_use_id ?? '').trim();
|
|
518
|
+
if (!id)
|
|
519
|
+
return;
|
|
520
|
+
if (subtype === 'task_started') {
|
|
521
|
+
(s.bgStarted ||= new Set()).add(id);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (isTerminalTaskStatus(ev?.patch?.status ?? ev?.status))
|
|
525
|
+
(s.bgTerminal ||= new Set()).add(id);
|
|
526
|
+
}
|
|
527
|
+
export function pendingClaudeBackgroundTasks(s) {
|
|
528
|
+
const started = s?.bgStarted;
|
|
529
|
+
if (!started?.size)
|
|
530
|
+
return 0;
|
|
531
|
+
const terminal = s?.bgTerminal;
|
|
532
|
+
let n = 0;
|
|
533
|
+
for (const id of started)
|
|
534
|
+
if (!terminal?.has(id))
|
|
535
|
+
n++;
|
|
536
|
+
return n;
|
|
537
|
+
}
|
|
538
|
+
// At a `result` event: settle the turn normally, unless background work is still running
|
|
539
|
+
// (then hold the process open for the wake-up). Errors always settle.
|
|
540
|
+
export function decideClaudeResultSettle(input) {
|
|
541
|
+
if (input.hasError)
|
|
542
|
+
return 'settle';
|
|
543
|
+
return input.pendingBackground > 0 ? 'hold' : 'settle';
|
|
544
|
+
}
|
|
545
|
+
// Claude vision input formats (matches the legacy driver / Anthropic API: png, jpeg, gif, webp).
|
|
546
|
+
const CLAUDE_IMAGE_MIME = {
|
|
547
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
548
|
+
'.gif': 'image/gif', '.webp': 'image/webp',
|
|
549
|
+
};
|
|
550
|
+
// A stream-json user message (for --input-format stream-json; used to send the prompt and to
|
|
551
|
+
// inject mid-turn steer messages while stdin stays open). Image attachments are inlined as base64
|
|
552
|
+
// image content blocks so the model actually sees them; other files become a text note. Without
|
|
553
|
+
// this the kernel path sent text only and silently dropped pasted/attached images.
|
|
554
|
+
export function claudeUserMessage(text, attachments) {
|
|
555
|
+
const content = [];
|
|
556
|
+
for (const filePath of attachments || []) {
|
|
557
|
+
const mime = CLAUDE_IMAGE_MIME[extname(filePath).toLowerCase()];
|
|
558
|
+
if (mime) {
|
|
559
|
+
try {
|
|
560
|
+
content.push({ type: 'image', source: { type: 'base64', media_type: mime, data: readFileSync(filePath).toString('base64') } });
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
catch { /* unreadable -> fall through to a text note */ }
|
|
564
|
+
}
|
|
565
|
+
content.push({ type: 'text', text: `[Attached file: ${filePath}]` });
|
|
566
|
+
}
|
|
567
|
+
content.push({ type: 'text', text });
|
|
568
|
+
return JSON.stringify({ type: 'user', message: { role: 'user', content } });
|
|
391
569
|
}
|
|
392
570
|
// Surface base64 image content blocks as artifacts (data URLs), deduped per turn.
|
|
393
571
|
export function emitClaudeImages(blocks, s, emit) {
|
|
@@ -425,6 +603,45 @@ export function todoWriteToPlan(input) {
|
|
|
425
603
|
}
|
|
426
604
|
return steps.length ? { explanation: null, steps } : null;
|
|
427
605
|
}
|
|
606
|
+
// ── Claude task-list (TaskCreate / TaskUpdate) -> UniversalPlan ──────────────────────
|
|
607
|
+
// Current Claude Code drives its task list through TaskCreate/TaskUpdate, NOT TodoWrite
|
|
608
|
+
// (TodoWrite is the legacy mechanism). TaskCreate carries the subject; its tool_result then
|
|
609
|
+
// assigns a stable task id (toolUseResult.task.id, or "Task #N" in the text). TaskUpdate flips
|
|
610
|
+
// a task's status by id. We accumulate the list in driver state and re-emit the whole plan on
|
|
611
|
+
// each change. Without this the kernel path never emits a plan event for Claude, so the
|
|
612
|
+
// dashboard's task-list card never renders. Ported from pikiloom's legacy claude driver.
|
|
613
|
+
// The assigned task id from a TaskCreate tool_result. Prefer the structured field; fall back
|
|
614
|
+
// to parsing "Task #N" from a string result.
|
|
615
|
+
export function readClaudeTaskCreateId(ev, block) {
|
|
616
|
+
const structured = ev?.toolUseResult?.task?.id;
|
|
617
|
+
if (structured != null && String(structured).trim())
|
|
618
|
+
return String(structured).trim();
|
|
619
|
+
const content = block?.content;
|
|
620
|
+
if (typeof content === 'string') {
|
|
621
|
+
const m = content.match(/Task #(\d+)/);
|
|
622
|
+
if (m)
|
|
623
|
+
return m[1];
|
|
624
|
+
}
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
export function rebuildClaudeTaskPlan(s) {
|
|
628
|
+
if (!Array.isArray(s?.taskOrder) || !s.taskOrder.length)
|
|
629
|
+
return null;
|
|
630
|
+
const steps = [];
|
|
631
|
+
for (const id of s.taskOrder) {
|
|
632
|
+
const task = s.taskList?.get(id);
|
|
633
|
+
if (!task)
|
|
634
|
+
continue;
|
|
635
|
+
const lowered = String(task.status || '').toLowerCase();
|
|
636
|
+
const status = lowered === 'completed' ? 'completed'
|
|
637
|
+
: (lowered === 'in_progress' || lowered === 'inprogress') ? 'inProgress'
|
|
638
|
+
: 'pending';
|
|
639
|
+
const text = String(task.subject || '').trim();
|
|
640
|
+
if (text)
|
|
641
|
+
steps.push({ text, status });
|
|
642
|
+
}
|
|
643
|
+
return steps.length ? { explanation: null, steps } : null;
|
|
644
|
+
}
|
|
428
645
|
// ── Tool-call summarization (ported from pikiloom's summarizeClaudeToolUse) ──────────
|
|
429
646
|
// Turns a Claude tool_use {name,input} into a one-line human summary. The runtime's
|
|
430
647
|
// activity projector joins these into snapshot.activity; the structured form lives in
|
|
@@ -241,7 +241,21 @@ export class CodexDriver {
|
|
|
241
241
|
const ok = await srv.start();
|
|
242
242
|
if (!ok)
|
|
243
243
|
return { ok: false, text: '', error: 'failed to start codex app-server', stopReason: 'error' };
|
|
244
|
-
|
|
244
|
+
let settle = () => { };
|
|
245
|
+
const turnDone = new Promise((res) => { settle = res; });
|
|
246
|
+
// On abort: gracefully interrupt the running turn, then settle turnDone OURSELVES. A bare
|
|
247
|
+
// srv.kill() (SIGTERM) never produces a turn/completed notification, so without this explicit
|
|
248
|
+
// settle() the `await turnDone` below hangs forever — run() never resolves and the task stays
|
|
249
|
+
// "running" in the orchestrator even though the codex process is already dead ("停止不掉,但实际上已经停了").
|
|
250
|
+
const onAbort = () => {
|
|
251
|
+
if (state.sessionId && state.turnId) {
|
|
252
|
+
srv.call('turn/interrupt', { threadId: state.sessionId, turnId: state.turnId }, 5_000).finally(() => settle());
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
srv.kill();
|
|
256
|
+
settle();
|
|
257
|
+
}
|
|
258
|
+
};
|
|
245
259
|
if (ctx.signal.aborted)
|
|
246
260
|
onAbort();
|
|
247
261
|
else
|
|
@@ -264,8 +278,6 @@ export class CodexDriver {
|
|
|
264
278
|
state.sessionId = threadId;
|
|
265
279
|
ctx.emit({ type: 'session', sessionId: threadId });
|
|
266
280
|
}
|
|
267
|
-
let settle = () => { };
|
|
268
|
-
const turnDone = new Promise((res) => { settle = res; });
|
|
269
281
|
srv.onNotification((method, params) => {
|
|
270
282
|
if (params?.threadId && params.threadId !== state.sessionId && method !== 'turn/started')
|
|
271
283
|
return;
|
|
@@ -9,7 +9,7 @@ export type { SessionStore, CoreSessionRecord, ModelResolver, ModelInjection, To
|
|
|
9
9
|
export type { LoomIO, PromptInput, Surface, SurfaceCapabilities, Plugin, SpawnContribution, } from './contracts/surface.js';
|
|
10
10
|
export { FsSessionStore, NullModelResolver, NoopToolProvider, PassthroughSystemPromptBuilder, AutoCancelInteractionHandler, DeferToTerminalInteractionHandler, NoopCatalog, defaultBaseDir, } from './ports/defaults.js';
|
|
11
11
|
export { EchoDriver } from './drivers/echo.js';
|
|
12
|
-
export { ClaudeDriver } from './drivers/claude.js';
|
|
12
|
+
export { ClaudeDriver, isTerminalTaskStatus, trackClaudeBackgroundTask, pendingClaudeBackgroundTasks, decideClaudeResultSettle, claudeBgHoldCapMs, type ClaudeResultSettleDecision, } from './drivers/claude.js';
|
|
13
13
|
export { CodexDriver } from './drivers/codex.js';
|
|
14
14
|
export { GeminiDriver } from './drivers/gemini.js';
|
|
15
15
|
export { AcpDriver, type AcpDriverConfig } from './drivers/acp.js';
|
|
@@ -19,7 +19,7 @@ export { attachTui } from './runtime/tui.js';
|
|
|
19
19
|
export { FsSessionStore, NullModelResolver, NoopToolProvider, PassthroughSystemPromptBuilder, AutoCancelInteractionHandler, DeferToTerminalInteractionHandler, NoopCatalog, defaultBaseDir, } from './ports/defaults.js';
|
|
20
20
|
// Drivers & surfaces (also available via subpath exports)
|
|
21
21
|
export { EchoDriver } from './drivers/echo.js';
|
|
22
|
-
export { ClaudeDriver } from './drivers/claude.js';
|
|
22
|
+
export { ClaudeDriver, isTerminalTaskStatus, trackClaudeBackgroundTask, pendingClaudeBackgroundTasks, decideClaudeResultSettle, claudeBgHoldCapMs, } from './drivers/claude.js';
|
|
23
23
|
export { CodexDriver } from './drivers/codex.js';
|
|
24
24
|
export { GeminiDriver } from './drivers/gemini.js';
|
|
25
25
|
export { AcpDriver } from './drivers/acp.js';
|