chatccc 0.2.286 → 0.2.287
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/deepccc-agent/package.json +1 -1
- package/dist/deepccc-agent/src/cli.js +22 -2
- package/dist/deepccc-agent/src/index.js +211 -139
- package/dist/deepccc-agent/src/progress/reducer.js +6 -0
- package/dist/src/adapters/ccc-adapter.js +7 -1
- package/dist/src/agent-activity.js +3 -0
- package/dist/src/cards.js +38 -0
- package/dist/src/execution-transcript.js +3 -0
- package/dist/src/orchestrator.js +34 -4
- package/dist/src/progress/reducer.js +6 -0
- package/dist/src/session-chat-binding.js +37 -0
- package/dist/src/session.js +26 -1
- package/package.json +1 -1
|
@@ -459,6 +459,10 @@ async function runRepl(args) {
|
|
|
459
459
|
}
|
|
460
460
|
let currentAbort = null;
|
|
461
461
|
const ctrlCState = createCtrlCState();
|
|
462
|
+
// 协作式让位:agent 运行期间用户仍可输入,新行进入注入队列,由内核在每个
|
|
463
|
+
// model step 边界 drain 后吸收到当前 turn(而非等待整轮结束)。
|
|
464
|
+
let chatRunning = false;
|
|
465
|
+
const pendingInjections = [];
|
|
462
466
|
rl.prompt();
|
|
463
467
|
rl.on("line", async (line) => {
|
|
464
468
|
ctrlCState.reset();
|
|
@@ -467,6 +471,17 @@ async function runRepl(args) {
|
|
|
467
471
|
rl.prompt();
|
|
468
472
|
return;
|
|
469
473
|
}
|
|
474
|
+
if (chatRunning) {
|
|
475
|
+
// 运行期间不并发启动新 turn:把输入注入当前 turn 的 step 边界。
|
|
476
|
+
if (input === "exit" || input.startsWith("/")) {
|
|
477
|
+
process.stdout.write(`${C.dim}[running] 命令将在本轮结束后生效;或按两次 Ctrl+C 中断${C.reset}\n`);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
pendingInjections.push(buildAttachmentPrompt(input, pendingAttachments));
|
|
481
|
+
pendingAttachments = [];
|
|
482
|
+
process.stdout.write(`${C.dim}[queued] 消息将在当前步骤后注入本轮${C.reset}\n`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
470
485
|
if (input === "exit") {
|
|
471
486
|
process.stdout.write(`${C.dim}bye${C.reset}\n`);
|
|
472
487
|
rl.close();
|
|
@@ -510,10 +525,11 @@ async function runRepl(args) {
|
|
|
510
525
|
const chatInput = buildAttachmentPrompt(input, pendingAttachments);
|
|
511
526
|
pendingAttachments = [];
|
|
512
527
|
let lastAccumulated = "";
|
|
513
|
-
|
|
528
|
+
chatRunning = true;
|
|
529
|
+
for await (const event of session.chat(chatInput, signal, () => pendingInjections.shift())) {
|
|
514
530
|
if (renderer && view) {
|
|
515
531
|
view = reduceProgress(view, event);
|
|
516
|
-
if (event.type === "text" || event.type === "compact" || event.type === "status") {
|
|
532
|
+
if (event.type === "text" || event.type === "compact" || event.type === "status" || event.type === "input_injected") {
|
|
517
533
|
renderer.render(view);
|
|
518
534
|
}
|
|
519
535
|
else {
|
|
@@ -543,6 +559,9 @@ async function runRepl(args) {
|
|
|
543
559
|
const status = event.is_error ? "error" : "ok";
|
|
544
560
|
console.log(`${C.dim}[tool result] ${event.name ?? event.tool_use_id} ${status}${C.reset}`);
|
|
545
561
|
}
|
|
562
|
+
else if (event.type === "input_injected") {
|
|
563
|
+
console.log(`\n${C.dim}[injected] ${event.text}${C.reset}`);
|
|
564
|
+
}
|
|
546
565
|
else if (event.type === "error") {
|
|
547
566
|
console.log(`\n${C.yellow}[error] ${event.message}${C.reset}`);
|
|
548
567
|
}
|
|
@@ -561,6 +580,7 @@ async function runRepl(args) {
|
|
|
561
580
|
finally {
|
|
562
581
|
activeRenderer = null;
|
|
563
582
|
activeView = null;
|
|
583
|
+
chatRunning = false;
|
|
564
584
|
if (renderer && view && !rendererEnded) {
|
|
565
585
|
// 定型终态区块(完成/已停止/异常结束)留在屏幕上,恢复光标
|
|
566
586
|
renderer.end(view);
|
|
@@ -246,6 +246,17 @@ function normalizeAnthropicBaseURL(baseURL) {
|
|
|
246
246
|
// DeepSeek Anthropic 端点示例:https://api.deepseek.com/anthropic/v1。
|
|
247
247
|
return baseURL.trim().replace(/\/+$/, "");
|
|
248
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* 协作式让位的内部中断信号:在 prepareStep(每个模型 step 前)检测到外部
|
|
251
|
+
* 注入消息时抛出,触发 chat() 持久化当前中间态 → 注入 user 消息 → 以新
|
|
252
|
+
* messages 重启 streamText,让 agent 在当前 turn 内吸收新指令而非结束整轮。
|
|
253
|
+
*/
|
|
254
|
+
class InputInjectionInterrupt extends Error {
|
|
255
|
+
constructor() {
|
|
256
|
+
super("input injection requested at step boundary");
|
|
257
|
+
this.name = "InputInjectionInterrupt";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
249
260
|
export class ChatSession {
|
|
250
261
|
model;
|
|
251
262
|
/** 子模型实例;未配置 subModel 时与主模型同一实例 */
|
|
@@ -396,7 +407,7 @@ export class ChatSession {
|
|
|
396
407
|
}
|
|
397
408
|
return systemContent.join("\n");
|
|
398
409
|
}
|
|
399
|
-
async *chat(userMessage, signal) {
|
|
410
|
+
async *chat(userMessage, signal, drainInput) {
|
|
400
411
|
this.context.appendMessage({ role: "user", content: userMessage });
|
|
401
412
|
let fullText = "";
|
|
402
413
|
let safeAccumulated = "";
|
|
@@ -469,6 +480,13 @@ export class ChatSession {
|
|
|
469
480
|
? { [OPENAI_COMPATIBLE_PROVIDER_NAME]: { reasoningEffort: this.effort } }
|
|
470
481
|
: { anthropic: { effort: this.effort } };
|
|
471
482
|
}
|
|
483
|
+
// 协作式让位(仅 streaming 模式):外部可在每个 model step 边界 drain 一条
|
|
484
|
+
// 新消息注入当前 turn。non-streaming 下 generateText 是原子请求,无法在 step
|
|
485
|
+
// 边界取回中间工具结果,因此忽略 drainInput(消息留在上层队列,整轮结束后消费)。
|
|
486
|
+
const canInject = this.streaming && typeof drainInput === "function";
|
|
487
|
+
// pendingInjection 是 prepareStep 与 catch 之间的注入信号槽。用标志而非
|
|
488
|
+
// instanceof 判断,因为 provider SDK 可能包装抛出的错误。
|
|
489
|
+
let pendingInjection = null;
|
|
472
490
|
const baseGenerationOptions = {
|
|
473
491
|
model: this.model,
|
|
474
492
|
system,
|
|
@@ -483,6 +501,13 @@ export class ChatSession {
|
|
|
483
501
|
stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
|
|
484
502
|
abortSignal: signal,
|
|
485
503
|
prepareStep: ({ messages, stepNumber }) => {
|
|
504
|
+
if (canInject) {
|
|
505
|
+
const injected = drainInput();
|
|
506
|
+
if (injected) {
|
|
507
|
+
pendingInjection = injected;
|
|
508
|
+
throw new InputInjectionInterrupt();
|
|
509
|
+
}
|
|
510
|
+
}
|
|
486
511
|
const compacted = compactToolLoopMessages(messages);
|
|
487
512
|
if (compacted.compactedResults > 0) {
|
|
488
513
|
rawLog?.writeLine(safeRawStreamJson({
|
|
@@ -500,151 +525,198 @@ export class ChatSession {
|
|
|
500
525
|
: {}),
|
|
501
526
|
...(effortProviderOptions ? { providerOptions: effortProviderOptions } : {}),
|
|
502
527
|
};
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
for await (const part of stream) {
|
|
533
|
-
rawLog?.writeLine(safeRawStreamJson(part));
|
|
534
|
-
if (part.type === "finish") {
|
|
535
|
-
receivedFinish = true;
|
|
536
|
-
finishReason = part.finishReason;
|
|
537
|
-
}
|
|
538
|
-
if (part.type === "reasoning-start" || part.type === "reasoning-delta") {
|
|
539
|
-
// Reasoning content remains private. A throttled heartbeat is enough
|
|
540
|
-
// for ChatCCC to distinguish active inference from a stalled stream.
|
|
541
|
-
const now = Date.now();
|
|
542
|
-
if (lastReasoningProgressAt === undefined || now - lastReasoningProgressAt >= 1_000) {
|
|
543
|
-
lastReasoningProgressAt = now;
|
|
544
|
-
yield { type: "progress", phase: "reasoning" };
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
else if (part.type === "text-delta") {
|
|
548
|
-
fullText += part.text;
|
|
549
|
-
const previous = timeline[timeline.length - 1];
|
|
550
|
-
if (previous?.type === "text")
|
|
551
|
-
previous.text += part.text;
|
|
552
|
-
else
|
|
553
|
-
timeline.push({ type: "text", text: part.text });
|
|
554
|
-
// 隐私替换只在展示层:safeAccumulated 供事件消费者(终端/JSONL)使用,
|
|
555
|
-
// fullText 原文用于持久化上下文,避免替换结果回流污染上下文。
|
|
556
|
-
const safeText = applyPrivacy(part.text);
|
|
557
|
-
safeAccumulated += safeText;
|
|
558
|
-
yield { type: "text", text: safeText, accumulated: safeAccumulated };
|
|
559
|
-
}
|
|
560
|
-
else if (part.type === "tool-call") {
|
|
561
|
-
const input = safeJson(part.input);
|
|
562
|
-
toolContext.push(`tool_call ${part.toolName}: ${input}`);
|
|
563
|
-
toolCallsById.set(part.toolCallId, { id: part.toolCallId, name: part.toolName, input });
|
|
564
|
-
toolCallOrder.push(part.toolCallId);
|
|
565
|
-
timeline.push({ type: "tool_use", id: part.toolCallId, name: part.toolName, input });
|
|
566
|
-
yield {
|
|
567
|
-
type: "tool_use",
|
|
568
|
-
id: part.toolCallId,
|
|
569
|
-
name: part.toolName,
|
|
570
|
-
input: applyPrivacyToJson(part.input),
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
else if (part.type === "tool-result") {
|
|
574
|
-
const output = truncateToolContext(safeJson(part.output));
|
|
575
|
-
toolContext.push(`tool_result ${part.toolName}: ${output}`);
|
|
576
|
-
const call = toolCallsById.get(part.toolCallId);
|
|
577
|
-
if (call)
|
|
578
|
-
call.output = output;
|
|
579
|
-
timeline.push({ type: "tool_result", tool_use_id: part.toolCallId, name: part.toolName, output });
|
|
580
|
-
yield {
|
|
581
|
-
type: "tool_result",
|
|
582
|
-
tool_use_id: part.toolCallId,
|
|
583
|
-
name: part.toolName,
|
|
584
|
-
content: applyPrivacyToJson(part.output),
|
|
585
|
-
is_error: false,
|
|
528
|
+
// 注入重启后 context 已变(中间 assistant + 注入 user),需要重新构建并应用
|
|
529
|
+
// 与首次一致的 recovery-hint / anthropic 兼容提示。
|
|
530
|
+
const decorateMessages = (msgs) => {
|
|
531
|
+
const hinted = maybeAppendCompactionRecoveryHint(msgs, this.context.summary, appConfig.rawStreamLogs.enabled, this.context.sessionId);
|
|
532
|
+
return this.provider === "anthropic"
|
|
533
|
+
? addAnthropicToolJsonCompatibilityNote(hinted)
|
|
534
|
+
: hinted;
|
|
535
|
+
};
|
|
536
|
+
let currentMessages = modelMessages;
|
|
537
|
+
while (true) {
|
|
538
|
+
pendingInjection = null;
|
|
539
|
+
try {
|
|
540
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
541
|
+
fullText = "";
|
|
542
|
+
toolContext = [];
|
|
543
|
+
toolCallsById.clear();
|
|
544
|
+
toolCallOrder.length = 0;
|
|
545
|
+
timeline.length = 0;
|
|
546
|
+
// safeAccumulated 是整轮展示累积:注入延续时不重置,只有工具协议恢复
|
|
547
|
+
// (重新生成、已 yield text_reset)才重置。
|
|
548
|
+
if (attempt === 1)
|
|
549
|
+
safeAccumulated = "";
|
|
550
|
+
let lastReasoningProgressAt;
|
|
551
|
+
const attemptMessages = attempt === 0
|
|
552
|
+
? currentMessages
|
|
553
|
+
: [...currentMessages, { role: "user", content: TOOL_PROTOCOL_RECOVERY_PROMPT }];
|
|
554
|
+
const generationOptions = {
|
|
555
|
+
...baseGenerationOptions,
|
|
556
|
+
messages: attemptMessages,
|
|
586
557
|
};
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
558
|
+
let stream;
|
|
559
|
+
let requiresFinish = false;
|
|
560
|
+
let receivedFinish = false;
|
|
561
|
+
let finishReason;
|
|
562
|
+
if (this.streaming) {
|
|
563
|
+
const result = streamText(generationOptions);
|
|
564
|
+
requiresFinish = result.fullStream != null;
|
|
565
|
+
stream = result.fullStream ?? textStreamToFullStream(result.textStream);
|
|
595
566
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
567
|
+
else {
|
|
568
|
+
const result = await generateText(generationOptions);
|
|
569
|
+
finishReason = result.finishReason;
|
|
570
|
+
stream = generateResultToFullStream(result);
|
|
571
|
+
}
|
|
572
|
+
for await (const part of stream) {
|
|
573
|
+
rawLog?.writeLine(safeRawStreamJson(part));
|
|
574
|
+
if (part.type === "finish") {
|
|
575
|
+
receivedFinish = true;
|
|
576
|
+
finishReason = part.finishReason;
|
|
577
|
+
}
|
|
578
|
+
if (part.type === "reasoning-start" || part.type === "reasoning-delta") {
|
|
579
|
+
// Reasoning content remains private. A throttled heartbeat is enough
|
|
580
|
+
// for ChatCCC to distinguish active inference from a stalled stream.
|
|
581
|
+
const now = Date.now();
|
|
582
|
+
if (lastReasoningProgressAt === undefined || now - lastReasoningProgressAt >= 1_000) {
|
|
583
|
+
lastReasoningProgressAt = now;
|
|
584
|
+
yield { type: "progress", phase: "reasoning" };
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
else if (part.type === "text-delta") {
|
|
588
|
+
fullText += part.text;
|
|
589
|
+
const previous = timeline[timeline.length - 1];
|
|
590
|
+
if (previous?.type === "text")
|
|
591
|
+
previous.text += part.text;
|
|
592
|
+
else
|
|
593
|
+
timeline.push({ type: "text", text: part.text });
|
|
594
|
+
// 隐私替换只在展示层:safeAccumulated 供事件消费者(终端/JSONL)使用,
|
|
595
|
+
// fullText 原文用于持久化上下文,避免替换结果回流污染上下文。
|
|
596
|
+
const safeText = applyPrivacy(part.text);
|
|
597
|
+
safeAccumulated += safeText;
|
|
598
|
+
yield { type: "text", text: safeText, accumulated: safeAccumulated };
|
|
599
|
+
}
|
|
600
|
+
else if (part.type === "tool-call") {
|
|
601
|
+
const input = safeJson(part.input);
|
|
602
|
+
toolContext.push(`tool_call ${part.toolName}: ${input}`);
|
|
603
|
+
toolCallsById.set(part.toolCallId, { id: part.toolCallId, name: part.toolName, input });
|
|
604
|
+
toolCallOrder.push(part.toolCallId);
|
|
605
|
+
timeline.push({ type: "tool_use", id: part.toolCallId, name: part.toolName, input });
|
|
606
|
+
yield {
|
|
607
|
+
type: "tool_use",
|
|
608
|
+
id: part.toolCallId,
|
|
609
|
+
name: part.toolName,
|
|
610
|
+
input: applyPrivacyToJson(part.input),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
else if (part.type === "tool-result") {
|
|
614
|
+
const output = truncateToolContext(safeJson(part.output));
|
|
615
|
+
toolContext.push(`tool_result ${part.toolName}: ${output}`);
|
|
616
|
+
const call = toolCallsById.get(part.toolCallId);
|
|
617
|
+
if (call)
|
|
618
|
+
call.output = output;
|
|
619
|
+
timeline.push({ type: "tool_result", tool_use_id: part.toolCallId, name: part.toolName, output });
|
|
620
|
+
yield {
|
|
621
|
+
type: "tool_result",
|
|
622
|
+
tool_use_id: part.toolCallId,
|
|
623
|
+
name: part.toolName,
|
|
624
|
+
content: applyPrivacyToJson(part.output),
|
|
625
|
+
is_error: false,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
else if (part.type === "tool-error") {
|
|
629
|
+
const message = errorMessage(part.error);
|
|
630
|
+
toolContext.push(`tool_error ${part.toolName}: ${message}`);
|
|
631
|
+
const call = toolCallsById.get(part.toolCallId);
|
|
632
|
+
if (call) {
|
|
633
|
+
call.output = message;
|
|
634
|
+
call.is_error = true;
|
|
635
|
+
}
|
|
636
|
+
timeline.push({ type: "tool_result", tool_use_id: part.toolCallId, name: part.toolName, output: message, is_error: true });
|
|
637
|
+
yield {
|
|
638
|
+
type: "tool_result",
|
|
639
|
+
tool_use_id: part.toolCallId,
|
|
640
|
+
name: part.toolName,
|
|
641
|
+
content: applyPrivacy(message),
|
|
642
|
+
is_error: true,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
else if (part.type === "error") {
|
|
646
|
+
if (pendingInjection !== null) {
|
|
647
|
+
// prepareStep 里的注入中断被 provider 转成了 error part。
|
|
648
|
+
throw new InputInjectionInterrupt();
|
|
649
|
+
}
|
|
650
|
+
const message = errorMessage(part.error);
|
|
651
|
+
yield { type: "error", message: applyPrivacy(message) };
|
|
652
|
+
throw new Error(message);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (!signal?.aborted) {
|
|
656
|
+
if (requiresFinish && !receivedFinish)
|
|
657
|
+
throw new Error("DeepCCC 输出流中断:未收到模型完成事件,回复可能不完整");
|
|
658
|
+
if (finishReason === "error" || finishReason === "length")
|
|
659
|
+
throw new Error(`DeepCCC 未正常完成:finishReason=${finishReason},回复可能不完整`);
|
|
660
|
+
if (!fullText.trim() && toolCallOrder.length === 0)
|
|
661
|
+
throw new Error("DeepCCC 本轮未产生有效回复");
|
|
662
|
+
}
|
|
663
|
+
if (hasMalformedToolProtocolText(fullText)) {
|
|
664
|
+
console.warn(`[DeepCCC] malformed tool protocol text detected for ${this.context.sessionId} `
|
|
665
|
+
+ `(attempt ${attempt + 1}/2, structuredToolCalls=${toolCallOrder.length})`);
|
|
666
|
+
rawLog?.writeLine(safeRawStreamJson({
|
|
667
|
+
type: "deepccc_tool_protocol_recovery",
|
|
668
|
+
attempt: attempt + 1,
|
|
669
|
+
structuredToolCalls: toolCallOrder.length,
|
|
670
|
+
}));
|
|
671
|
+
yield { type: "text_reset" };
|
|
672
|
+
if (attempt === 0 && toolCallOrder.length === 0) {
|
|
673
|
+
yield { type: "status", phase: "generating" };
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
throw new Error(toolCallOrder.length > 0
|
|
677
|
+
? "工具调用协议异常:检测到混合的结构化调用与伪造工具文本,为避免重复执行工具,本轮已安全终止"
|
|
678
|
+
: "工具调用协议异常:模型重试后仍输出了无效或伪造的工具调用文本");
|
|
679
|
+
}
|
|
680
|
+
completed = true;
|
|
681
|
+
const collectedToolCalls = toolCallOrder
|
|
682
|
+
.map((id) => toolCallsById.get(id))
|
|
683
|
+
.filter((call) => call !== undefined);
|
|
684
|
+
this.context.appendMessage(buildPersistedAssistantMessage({
|
|
685
|
+
fullText,
|
|
686
|
+
transcriptLines: toolContext,
|
|
687
|
+
toolCalls: collectedToolCalls,
|
|
688
|
+
timeline,
|
|
689
|
+
}));
|
|
690
|
+
yield { type: "done", text: safeAccumulated };
|
|
691
|
+
return;
|
|
609
692
|
}
|
|
610
693
|
}
|
|
611
|
-
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
694
|
+
catch (err) {
|
|
695
|
+
if (pendingInjection !== null) {
|
|
696
|
+
// 协作式让位:持久化本段中间态 → 注入 user → 重建 messages → 继续当前 turn。
|
|
697
|
+
if (fullText.trim() || toolCallOrder.length > 0) {
|
|
698
|
+
const collectedToolCalls = toolCallOrder
|
|
699
|
+
.map((id) => toolCallsById.get(id))
|
|
700
|
+
.filter((call) => call !== undefined);
|
|
701
|
+
this.context.appendMessage(buildPersistedAssistantMessage({
|
|
702
|
+
fullText,
|
|
703
|
+
transcriptLines: toolContext,
|
|
704
|
+
toolCalls: collectedToolCalls,
|
|
705
|
+
timeline: timeline.map((entry) => ({ ...entry })),
|
|
706
|
+
}));
|
|
707
|
+
}
|
|
708
|
+
const injectedText = pendingInjection;
|
|
709
|
+
this.context.appendMessage({ role: "user", content: injectedText });
|
|
710
|
+
rawLog?.writeLine(safeRawStreamJson({
|
|
711
|
+
type: "deepccc_input_injected",
|
|
712
|
+
text: injectedText,
|
|
713
|
+
}));
|
|
714
|
+
yield { type: "input_injected", text: injectedText };
|
|
715
|
+
currentMessages = decorateMessages(this.context.buildModelMessages());
|
|
630
716
|
continue;
|
|
631
717
|
}
|
|
632
|
-
throw
|
|
633
|
-
? "工具调用协议异常:检测到混合的结构化调用与伪造工具文本,为避免重复执行工具,本轮已安全终止"
|
|
634
|
-
: "工具调用协议异常:模型重试后仍输出了无效或伪造的工具调用文本");
|
|
718
|
+
throw err;
|
|
635
719
|
}
|
|
636
|
-
completed = true;
|
|
637
|
-
const collectedToolCalls = toolCallOrder
|
|
638
|
-
.map((id) => toolCallsById.get(id))
|
|
639
|
-
.filter((call) => call !== undefined);
|
|
640
|
-
this.context.appendMessage(buildPersistedAssistantMessage({
|
|
641
|
-
fullText,
|
|
642
|
-
transcriptLines: toolContext,
|
|
643
|
-
toolCalls: collectedToolCalls,
|
|
644
|
-
timeline,
|
|
645
|
-
}));
|
|
646
|
-
yield { type: "done", text: safeAccumulated };
|
|
647
|
-
return;
|
|
648
720
|
}
|
|
649
721
|
}
|
|
650
722
|
catch (err) {
|
|
@@ -102,5 +102,11 @@ export function reduceProgress(prev, event) {
|
|
|
102
102
|
case "compact":
|
|
103
103
|
// 旧上下文压缩不影响当前过程展示
|
|
104
104
|
return prev;
|
|
105
|
+
case "input_injected": {
|
|
106
|
+
// 协作式让位:在当前 turn 的 step 边界注入了新消息,仅在头部提示,
|
|
107
|
+
// 不改动正文与工具状态。
|
|
108
|
+
const preview = event.text.length > 40 ? `${event.text.slice(0, 40)}…` : event.text;
|
|
109
|
+
return withProgressView(prev, { headerTitle: `已注入新消息:${preview}` });
|
|
110
|
+
}
|
|
105
111
|
}
|
|
106
112
|
}
|
|
@@ -52,7 +52,7 @@ export function createCccAdapter(options = {}) {
|
|
|
52
52
|
const normalizedSessionId = normalizeBuiltinSessionId(sessionId);
|
|
53
53
|
const session = new ChatSession(chatConfig, toChatSessionOptions(normalizedSessionId, cwd, options));
|
|
54
54
|
const completion = createTurnCompletion("CCC Agent");
|
|
55
|
-
for await (const event of session.chat(userText, signal)) {
|
|
55
|
+
for await (const event of session.chat(userText, signal, _promptOptions?.drainInput)) {
|
|
56
56
|
if (event.type === "text")
|
|
57
57
|
completion.observe({ type: "assistant", blocks: [{ type: "text", text: event.text }] });
|
|
58
58
|
if (event.type === "text_reset")
|
|
@@ -78,6 +78,12 @@ export function createCccAdapter(options = {}) {
|
|
|
78
78
|
blocks: [{ type: "text_reset" }],
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
|
+
else if (event.type === "input_injected") {
|
|
82
|
+
yield {
|
|
83
|
+
type: "assistant",
|
|
84
|
+
blocks: [{ type: "input_injected", text: event.text }],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
81
87
|
else if (event.type === "text") {
|
|
82
88
|
yield {
|
|
83
89
|
type: "assistant",
|
|
@@ -88,6 +88,9 @@ export function updateAgentActivity(tracker, block, now = Date.now()) {
|
|
|
88
88
|
return setActivity(tracker, { kind: "searching", startedAt: now });
|
|
89
89
|
case "compact_boundary":
|
|
90
90
|
return setActivity(tracker, { kind: "compacting", startedAt: now });
|
|
91
|
+
case "input_injected":
|
|
92
|
+
// 协作式让位:注入本身不是 agent 活动,不改变状态标题。
|
|
93
|
+
return false;
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
96
|
function formatElapsed(startedAt, now) {
|
package/dist/src/cards.js
CHANGED
|
@@ -394,6 +394,44 @@ export function buildQueueFullCard() {
|
|
|
394
394
|
],
|
|
395
395
|
});
|
|
396
396
|
}
|
|
397
|
+
// 协作式让位卡片(仅 ccc):消息进入注入队列,将在 step 边界吸收进本轮。
|
|
398
|
+
// 与上面的整轮队列卡片区分,强调“无需等待整轮结束”。
|
|
399
|
+
export function buildInjectionQueuedCard(text) {
|
|
400
|
+
const preview = text.length > 100 ? text.slice(0, 100) + "…" : text;
|
|
401
|
+
return JSON.stringify({
|
|
402
|
+
config: { wide_screen_mode: true },
|
|
403
|
+
header: { template: "blue", title: { content: "消息将注入本轮", tag: "plain_text" } },
|
|
404
|
+
elements: [
|
|
405
|
+
{ tag: "div", text: { tag: "lark_md", content: `当前会话正在生成中,你的消息会在**当前步骤结束后立即注入本轮**,无需等待整轮结束。\n\n> ${preview}` } },
|
|
406
|
+
{ tag: "hr" },
|
|
407
|
+
{
|
|
408
|
+
tag: "action",
|
|
409
|
+
actions: [
|
|
410
|
+
{ tag: "button", text: { tag: "plain_text", content: "清空注入(/cancel)" }, type: "danger", value: { action: "cancel" } },
|
|
411
|
+
{ tag: "button", text: { tag: "plain_text", content: "停止生成(/stop)" }, type: "default", value: { action: "stop" } },
|
|
412
|
+
],
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
// 注入队列满卡片(仅 ccc)
|
|
418
|
+
export function buildInjectionQueueFullCard() {
|
|
419
|
+
return JSON.stringify({
|
|
420
|
+
config: { wide_screen_mode: true },
|
|
421
|
+
header: { template: "yellow", title: { content: "待注入消息过多", tag: "plain_text" } },
|
|
422
|
+
elements: [
|
|
423
|
+
{ tag: "div", text: { tag: "lark_md", content: "当前已有较多消息等待注入本轮,请稍候或发送指令:\n- **/stop** — 停止当前生成\n- **/cancel** — 清空待注入消息" } },
|
|
424
|
+
{ tag: "hr" },
|
|
425
|
+
{
|
|
426
|
+
tag: "action",
|
|
427
|
+
actions: [
|
|
428
|
+
{ tag: "button", text: { tag: "plain_text", content: "清空注入(/cancel)" }, type: "danger", value: { action: "cancel" } },
|
|
429
|
+
{ tag: "button", text: { tag: "plain_text", content: "停止生成(/stop)" }, type: "default", value: { action: "stop" } },
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
});
|
|
434
|
+
}
|
|
397
435
|
// 状态卡片(带关闭按钮)
|
|
398
436
|
export function buildStatusCard(statusText, template = "blue") {
|
|
399
437
|
return JSON.stringify({
|
|
@@ -58,6 +58,9 @@ export function appendExecutionTranscriptBlock(block, state, at = new Date().toI
|
|
|
58
58
|
// Heartbeats carry no content and can occur very frequently. Persisting them
|
|
59
59
|
// would add noise without helping users reconstruct what happened.
|
|
60
60
|
return;
|
|
61
|
+
case "input_injected":
|
|
62
|
+
appendEntry(state, { type: "notice", at, text: `已注入新消息:${block.text}` });
|
|
63
|
+
return;
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
export function isExecutionTranscriptEntry(value) {
|
package/dist/src/orchestrator.js
CHANGED
|
@@ -14,10 +14,10 @@ import { withGitCoAuthor } from "../deepccc-agent/src/file-tools.js";
|
|
|
14
14
|
import { makeTraceId, logTrace } from "./trace.js";
|
|
15
15
|
import { appendStartupTrace } from "./shared.js";
|
|
16
16
|
import { CLAUDE_MODEL, GIT_TIMEOUT_MS, PROJECT_ROOT, anthropicConfigDisplay, config, fileLog, getAllEffortsForTool, getAllModelsForTool, getDefaultEffortForTool, getDefaultCwd, LOG_DIR, setDefaultCwd, getRecentDirs, addRecentDir, resolveDefaultAgentTool, sessionPrefixForTool, toolDisplayName, ts, } from "./config.js";
|
|
17
|
-
import { buildHelpCard, buildEffortCard, buildFastModeCard, buildModelCard, buildStatusCard, buildCdContent, buildCdCard, buildSessionsCard, buildQueuedCard, buildQueueFullCard, buildCodexUsageCard, } from "./cards.js";
|
|
17
|
+
import { buildHelpCard, buildEffortCard, buildFastModeCard, buildModelCard, buildStatusCard, buildCdContent, buildCdCard, buildSessionsCard, buildQueuedCard, buildQueueFullCard, buildInjectionQueuedCard, buildInjectionQueueFullCard, buildCodexUsageCard, } from "./cards.js";
|
|
18
18
|
import { formatGitResult, gitResultHeaderTemplate, runGitCommand, } from "./git-command.js";
|
|
19
19
|
import { clearSessionModelOverride, clearSessionEffortOverride, getSessionStatus, getAllSessionsStatus, initClaudeSession, lastMsgTimestamps, resumeAndPrompt, sessionInfoMap, setSessionModelOverride, setSessionEffortOverride, switchChatBinding, recordSessionRegistry, getAdapterForTool, getEffectiveModelForTool, getEffectiveEffortForTool, getEffectiveFastModeForTool, setSessionFastModeOverride, stopSession, loadSessionRegistryForBinding, removeSessionRegistryRecord, saveSessionTool, saveSessionPresentation, recordChatPlatform, } from "./session.js";
|
|
20
|
-
import { bindChatToSession, unbindChatFromSession, isSessionRunning, displayCards, enqueueMessage, cancelQueuedMessage, getSessionDrainSnapshot, } from "./session-chat-binding.js";
|
|
20
|
+
import { bindChatToSession, unbindChatFromSession, isSessionRunning, displayCards, enqueueMessage, cancelQueuedMessage, pushInjection, clearInjections, getSessionDrainSnapshot, } from "./session-chat-binding.js";
|
|
21
21
|
import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.js";
|
|
22
22
|
import { getCursorUsageSummary } from "./cursor-usage.js";
|
|
23
23
|
import { getChatGptSubscriptionStatus } from "./chatgpt-subscription.js";
|
|
@@ -1675,9 +1675,11 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
|
|
|
1675
1675
|
}
|
|
1676
1676
|
if (isCommandText && textLower === "/cancel") {
|
|
1677
1677
|
logTrace(tid, "BRANCH", { cmd: "/cancel" });
|
|
1678
|
-
|
|
1678
|
+
const cancelledQueue = cancelQueuedMessage(sessionId);
|
|
1679
|
+
const cancelledInjections = clearInjections(sessionId);
|
|
1680
|
+
if (cancelledQueue || cancelledInjections) {
|
|
1679
1681
|
console.log(`[${ts()}] [CANCEL] Queue cancelled for session=${sessionId}`);
|
|
1680
|
-
await platform.sendText(chatId, "
|
|
1682
|
+
await platform.sendText(chatId, "已取消缓存队列与待注入的消息。").catch(() => { });
|
|
1681
1683
|
logTrace(tid, "DONE", { outcome: "cancelled" });
|
|
1682
1684
|
}
|
|
1683
1685
|
else {
|
|
@@ -2237,6 +2239,34 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
|
|
|
2237
2239
|
}
|
|
2238
2240
|
// 并发检查:同一 session 只能有一个活跃 prompt,多余消息进入队列
|
|
2239
2241
|
if (isSessionRunning(sessionId)) {
|
|
2242
|
+
// ccc 内核支持协作式让位:运行期新消息进入注入队列,由 drainInput 在每个
|
|
2243
|
+
// model step 边界逐条吸收进当前 turn(不新开 turn)。其他 agent 保持整轮队列。
|
|
2244
|
+
if (descriptionTool === "ccc") {
|
|
2245
|
+
const injected = pushInjection(sessionId, {
|
|
2246
|
+
text: promptText, chatId, openId, msgTimestamp, chatType, traceId: tid,
|
|
2247
|
+
});
|
|
2248
|
+
if (injected) {
|
|
2249
|
+
logTrace(tid, "INJECT_QUEUED", { sessionId });
|
|
2250
|
+
console.log(`[${ts()}] [INJECT_QUEUED] Session ${sessionId} (ccc) busy, message from chat ${chatId} queued for step-boundary injection`);
|
|
2251
|
+
if (platform.kind === "wechat") {
|
|
2252
|
+
await platform.sendText(chatId, "当前会话正在生成中,你的消息会在当前步骤结束后注入本轮处理。").catch(() => { });
|
|
2253
|
+
}
|
|
2254
|
+
else {
|
|
2255
|
+
await platform.sendRawCard(chatId, buildInjectionQueuedCard(text)).catch(() => { });
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
else {
|
|
2259
|
+
logTrace(tid, "INJECT_QUEUE_FULL", { sessionId });
|
|
2260
|
+
console.log(`[${ts()}] [INJECT_QUEUE_FULL] Session ${sessionId} (ccc) injection queue full, rejecting message from chat ${chatId}`);
|
|
2261
|
+
if (platform.kind === "wechat") {
|
|
2262
|
+
await platform.sendText(chatId, "当前待注入消息过多,请等待或发送 /stop(停止生成)或 /cancel(清空注入)。").catch(() => { });
|
|
2263
|
+
}
|
|
2264
|
+
else {
|
|
2265
|
+
await platform.sendRawCard(chatId, buildInjectionQueueFullCard()).catch(() => { });
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2240
2270
|
const queued = enqueueMessage(sessionId, {
|
|
2241
2271
|
text: promptText, chatId, openId, msgTimestamp, chatType, traceId: tid,
|
|
2242
2272
|
});
|
|
@@ -102,5 +102,11 @@ export function reduceProgress(prev, event) {
|
|
|
102
102
|
case "compact":
|
|
103
103
|
// 旧上下文压缩不影响当前过程展示
|
|
104
104
|
return prev;
|
|
105
|
+
case "input_injected": {
|
|
106
|
+
// 协作式让位:在当前 turn 的 step 边界注入了新消息,仅在头部提示,
|
|
107
|
+
// 不改动正文与工具状态。
|
|
108
|
+
const preview = event.text.length > 40 ? `${event.text.slice(0, 40)}…` : event.text;
|
|
109
|
+
return withProgressView(prev, { headerTitle: `已注入新消息:${preview}` });
|
|
110
|
+
}
|
|
105
111
|
}
|
|
106
112
|
}
|
|
@@ -163,6 +163,42 @@ export function hasQueuedMessage(sessionId) {
|
|
|
163
163
|
return queuedMessages.has(sessionId);
|
|
164
164
|
}
|
|
165
165
|
// ---------------------------------------------------------------------------
|
|
166
|
+
// pendingInjections: sessionId → 运行中待注入消息(仅 ccc 内核)
|
|
167
|
+
// 与上面的 queuedMessages(整轮队列,深度 1)分离:ccc 运行期的新消息进入
|
|
168
|
+
// 这里,由 drainInput 在每个 model step 边界逐条吸收进当前 turn;其他 agent
|
|
169
|
+
// 继续走整轮队列。turn 结束后剩余未注入的消息转回普通队列消费。
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
export const MAX_PENDING_INJECTIONS = 50;
|
|
172
|
+
export const pendingInjections = new Map();
|
|
173
|
+
export function pushInjection(sessionId, msg) {
|
|
174
|
+
const list = pendingInjections.get(sessionId) ?? [];
|
|
175
|
+
if (list.length >= MAX_PENDING_INJECTIONS)
|
|
176
|
+
return false;
|
|
177
|
+
list.push(msg);
|
|
178
|
+
pendingInjections.set(sessionId, list);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
export function shiftInjection(sessionId) {
|
|
182
|
+
const list = pendingInjections.get(sessionId);
|
|
183
|
+
if (!list || list.length === 0)
|
|
184
|
+
return undefined;
|
|
185
|
+
const msg = list.shift();
|
|
186
|
+
if (list.length === 0)
|
|
187
|
+
pendingInjections.delete(sessionId);
|
|
188
|
+
return msg;
|
|
189
|
+
}
|
|
190
|
+
export function drainRemainingInjections(sessionId) {
|
|
191
|
+
const list = pendingInjections.get(sessionId);
|
|
192
|
+
pendingInjections.delete(sessionId);
|
|
193
|
+
return list ?? [];
|
|
194
|
+
}
|
|
195
|
+
export function hasPendingInjection(sessionId) {
|
|
196
|
+
return (pendingInjections.get(sessionId)?.length ?? 0) > 0;
|
|
197
|
+
}
|
|
198
|
+
export function clearInjections(sessionId) {
|
|
199
|
+
pendingInjections.delete(sessionId);
|
|
200
|
+
}
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
166
202
|
// 队列消费回调(由 index.ts 注入,避免 session.ts → orchestrator.ts 循环依赖)
|
|
167
203
|
// ---------------------------------------------------------------------------
|
|
168
204
|
let onConsumeQueuedMessage = null;
|
|
@@ -187,6 +223,7 @@ export function resetBindingState() {
|
|
|
187
223
|
finalizingSessions.clear();
|
|
188
224
|
autoRecoveryReservations.clear();
|
|
189
225
|
queuedMessages.clear();
|
|
226
|
+
pendingInjections.clear();
|
|
190
227
|
displayCards.clear();
|
|
191
228
|
if (unifiedDisplayLoopHandle !== null) {
|
|
192
229
|
clearInterval(unifiedDisplayLoopHandle);
|
package/dist/src/session.js
CHANGED
|
@@ -29,7 +29,7 @@ function compressWechatDisplayText(text) {
|
|
|
29
29
|
}
|
|
30
30
|
import { readStreamState, writeStreamState, createEmptyStreamState, isFinalReplySentForTurn, markFinalReplySent, } from "./stream-state.js";
|
|
31
31
|
import { addCardToTurn, finalizeTurnCards, markCardDone } from "./turn-cards.js";
|
|
32
|
-
import { bindChatToSession, unbindChatFromSession, getChatsForSession, activePrompts, displayCards, unifiedDisplayLoopHandle, setUnifiedDisplayLoopHandle, rebuildSessionChatsFromRegistry, recordLastActiveChat, getLastActiveChat, pickDisplayChat, dequeueMessage, consumeQueuedMessage, cancelQueuedMessage, setQueuePreservedChat, consumeQueuePreservedChat, markSessionFinalizing, clearSessionFinalizing, reserveAutoRecovery, consumeAutoRecoveryReservation, cancelAutoRecoveryReservation, hasAutoRecoveryReservation, } from "./session-chat-binding.js";
|
|
32
|
+
import { bindChatToSession, unbindChatFromSession, getChatsForSession, activePrompts, displayCards, unifiedDisplayLoopHandle, setUnifiedDisplayLoopHandle, rebuildSessionChatsFromRegistry, recordLastActiveChat, getLastActiveChat, pickDisplayChat, dequeueMessage, consumeQueuedMessage, cancelQueuedMessage, shiftInjection, drainRemainingInjections, clearInjections, setQueuePreservedChat, consumeQueuePreservedChat, markSessionFinalizing, clearSessionFinalizing, reserveAutoRecovery, consumeAutoRecoveryReservation, cancelAutoRecoveryReservation, hasAutoRecoveryReservation, } from "./session-chat-binding.js";
|
|
33
33
|
async function sendFinalReplyTextOnce(platform, chatId, sessionId, turnCount, text) {
|
|
34
34
|
const sent = await platform.sendText(chatId, text).then((ok) => ok !== false).catch(() => false);
|
|
35
35
|
if (sent)
|
|
@@ -827,6 +827,10 @@ export function accumulateBlockContent(block, state, toolCallMap) {
|
|
|
827
827
|
}
|
|
828
828
|
case "agent_status":
|
|
829
829
|
break;
|
|
830
|
+
case "input_injected":
|
|
831
|
+
// 协作式让位:显示一条轻量注入标记(仅过程展示,不进入最终回复)。
|
|
832
|
+
state.accumulatedContent += `\n\n↳ 已注入新消息:${block.text}\n`;
|
|
833
|
+
break;
|
|
830
834
|
}
|
|
831
835
|
}
|
|
832
836
|
export async function switchChatBinding(args) {
|
|
@@ -1299,6 +1303,16 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1299
1303
|
if (prompt)
|
|
1300
1304
|
prompt.closeSession = closeSession;
|
|
1301
1305
|
},
|
|
1306
|
+
// ccc 内核支持协作式让位:同步从注入队列取队首文本,供内核在每个
|
|
1307
|
+
// model step 边界吸收。非 ccc adapter 忽略该字段。
|
|
1308
|
+
drainInput: tool === "ccc" ? () => {
|
|
1309
|
+
const injected = shiftInjection(sessionId);
|
|
1310
|
+
if (!injected)
|
|
1311
|
+
return undefined;
|
|
1312
|
+
// 与首次消息保持一致的结构化包装;imSkillsPrompt 已在首次消息中,
|
|
1313
|
+
// 此处不重复注入,避免多轮注入导致上下文膨胀。
|
|
1314
|
+
return `[User message]\n${injected.text}\n[/User message]`;
|
|
1315
|
+
} : undefined,
|
|
1302
1316
|
})) {
|
|
1303
1317
|
if (unifiedMsg.isFinalResponse) {
|
|
1304
1318
|
const prompt = activePrompts.get(sessionId);
|
|
@@ -1630,11 +1644,20 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1630
1644
|
if (discarded) {
|
|
1631
1645
|
console.log(`[${ts()}] [QUEUE] Discarding queued message for stopped session ${sessionId}`);
|
|
1632
1646
|
}
|
|
1647
|
+
const discardedInjections = drainRemainingInjections(sessionId);
|
|
1648
|
+
if (discardedInjections.length > 0) {
|
|
1649
|
+
console.log(`[${ts()}] [INJECT] Discarding ${discardedInjections.length} pending injection(s) for stopped session ${sessionId}`);
|
|
1650
|
+
}
|
|
1633
1651
|
}
|
|
1634
1652
|
else if (!shouldScheduleAutoRecovery) {
|
|
1635
1653
|
// 第一次 response-stall 后保留普通缓存;恢复轮结束后由恢复轮的
|
|
1636
1654
|
// finally 再消费,顺序固定为“自动恢复 → 用户缓存”。
|
|
1637
1655
|
queuedForConsumption = dequeueMessage(sessionId);
|
|
1656
|
+
// ccc 注入队列剩余(turn 期间未注入完,如 non-streaming 或收尾窗口到达)
|
|
1657
|
+
// 取第一条转普通消费,其余留待下一轮结束后继续消费。
|
|
1658
|
+
if (!queuedForConsumption) {
|
|
1659
|
+
queuedForConsumption = shiftInjection(sessionId);
|
|
1660
|
+
}
|
|
1638
1661
|
}
|
|
1639
1662
|
if (queuedForConsumption) {
|
|
1640
1663
|
const queued = queuedForConsumption;
|
|
@@ -2043,6 +2066,7 @@ export function stopSession(sessionId) {
|
|
|
2043
2066
|
if (!prompt) {
|
|
2044
2067
|
if (cancelledRecovery) {
|
|
2045
2068
|
cancelQueuedMessage(sessionId);
|
|
2069
|
+
clearInjections(sessionId);
|
|
2046
2070
|
console.log(`[${ts()}] [STOP] Reserved automatic recovery for ${sessionId} cancelled`);
|
|
2047
2071
|
return true;
|
|
2048
2072
|
}
|
|
@@ -2054,6 +2078,7 @@ export function stopSession(sessionId) {
|
|
|
2054
2078
|
clearPromptAvatarRefreshTimer(sessionId);
|
|
2055
2079
|
clearPromptFinalResponseCloseTimer(sessionId);
|
|
2056
2080
|
cancelQueuedMessage(sessionId);
|
|
2081
|
+
clearInjections(sessionId);
|
|
2057
2082
|
// 先发起整棵进程树清理,再触发 close/abort。Windows 上 CLI 由
|
|
2058
2083
|
// cmd.exe → node → 实际二进制组成;若先 process.kill(cmd.exe),taskkill
|
|
2059
2084
|
// 随后便无法从已消失的根 PID 找到后代,正是幽灵 Codex/Cursor 的来源。
|