handmux 0.29.2 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connectors/claude/index.js +33 -562
- package/dist/connectors/codebuddy/index.js +49 -0
- package/dist/connectors/hookBridge.js +560 -0
- package/dist/hooks/handmux-codebuddy-notify.sh +116 -0
- package/dist/hooks/handmux-notify.sh +2 -0
- package/dist/hooks/handmux-write.cjs +72 -5
- package/dist/package.json +13 -13
- package/dist/public/assets/_basePickBy-BzC0P9Wg.js +1 -0
- package/dist/public/assets/_baseUniq-DNOfPYVY.js +1 -0
- package/dist/public/assets/abnfDiagram-O67JEVCF-BsRDIQEB.js +1 -0
- package/dist/public/assets/arc-DLtLfMnX.js +1 -0
- package/dist/public/assets/architectureDiagram-NJMV4G6O-DoIXhWqZ.js +36 -0
- package/dist/public/assets/blockDiagram-BEXU5L5S-W0HiOcVA.js +140 -0
- package/dist/public/assets/c4Diagram-YGBWAQC7-72uAUMvX.js +38 -0
- package/dist/public/assets/channel-CzALSnb0.js +1 -0
- package/dist/public/assets/chunk-2Q5K7J3B-B9wSRFQ0.js +1 -0
- package/dist/public/assets/chunk-3FUC2YCW-ryphp4s0.js +2 -0
- package/dist/public/assets/chunk-5VM5RSS4-Cm4KeKby.js +15 -0
- package/dist/public/assets/chunk-GTNCS2PH-Du4_i7r3.js +88 -0
- package/dist/public/assets/chunk-GWA4HPMP-CL1biVIA.js +1 -0
- package/dist/public/assets/chunk-JWPE2WC7-B8eAVfuB.js +1 -0
- package/dist/public/assets/chunk-NETBCI7D-D273GzYg.js +1 -0
- package/dist/public/assets/chunk-WEXAMYUT-DipTfRbc.js +1 -0
- package/dist/public/assets/chunk-XXDRQBXY-D4SqXrQ8.js +1 -0
- package/dist/public/assets/classDiagram-v2-NBCMYWYE-PrpnIXcA.js +217 -0
- package/dist/public/assets/cose-bilkent-JH36ORCC-BfcYMYTa.js +1 -0
- package/dist/public/assets/cynefin-OW5HDTMX-Du659bmz.js +178 -0
- package/dist/public/assets/cynefinDiagram-VND7K2PF-DR_2Copy.js +62 -0
- package/dist/public/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/dist/public/assets/dagre-6A5THRUB-COsChaV5.js +4 -0
- package/dist/public/assets/defaultLocale-CrowFXzY.js +1 -0
- package/dist/public/assets/diagram-22UHCM2B-Cx2T1HvL.js +200 -0
- package/dist/public/assets/diagram-3UASUU5V-Cp04LOka.js +24 -0
- package/dist/public/assets/diagram-ATOU4E4O-DlqFSshY.js +3 -0
- package/dist/public/assets/diagram-CDSNMT55-goq40ctA.js +30 -0
- package/dist/public/assets/diagram-MLGK6HIB-Bdzjsy4r.js +24 -0
- package/dist/public/assets/diagram-MPIPVDR6-DtU7cM6P.js +41 -0
- package/dist/public/assets/ebnfDiagram-ZINNZB2B-XyVCfmED.js +1 -0
- package/dist/public/assets/elk-276RUBZZ-aHVIw-2x.js +27 -0
- package/dist/public/assets/erDiagram-OPXOYQCR-Bq_PhTIL.js +99 -0
- package/dist/public/assets/flowDiagram-KWPJA3E3-WuqnDSiF.js +213 -0
- package/dist/public/assets/ganttDiagram-FUAMR5RP-DZ4lXzkC.js +292 -0
- package/dist/public/assets/gitGraphDiagram-X574FWY7-t6l07b9g.js +106 -0
- package/dist/public/assets/graph-jWsD5Ro-.js +1 -0
- package/dist/public/assets/index-DiYff8oR.css +32 -0
- package/dist/public/assets/index-YZNK723o.js +203 -0
- package/dist/public/assets/infoDiagram-VRGFBTTK-iUGqoOFp.js +2 -0
- package/dist/public/assets/init-Gi6I4Gst.js +1 -0
- package/dist/public/assets/ishikawaDiagram-OU5B5YK6-B0TmoUOK.js +70 -0
- package/dist/public/assets/journeyDiagram-ZHPQQLJL-CQE61MCu.js +139 -0
- package/dist/public/assets/kanban-definition-PNTS6WVX-D2MWCUyS.js +89 -0
- package/dist/public/assets/katex-C5jXJg4s.js +257 -0
- package/dist/public/assets/linear-WKGxVNP1.js +1 -0
- package/dist/public/assets/mermaid.core-DPEwUsaj.js +311 -0
- package/dist/public/assets/mindmap-definition-NLK3R4M7-DsYEsBsI.js +96 -0
- package/dist/public/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/public/assets/pegDiagram-GJSIUBJH-BS52Fbxt.js +1 -0
- package/dist/public/assets/pieDiagram-5QR66LMP-D4o8g7nW.js +39 -0
- package/dist/public/assets/quadrantDiagram-O4NWA36T-DnYUxWll.js +7 -0
- package/dist/public/assets/railroadDiagram-XR7U4H2S-BKy_7ULV.js +1 -0
- package/dist/public/assets/requirementDiagram-PLB6GJNP-Ce5F4u8K.js +84 -0
- package/dist/public/assets/sankeyDiagram-IPEJSGJF-CCJDrut_.js +40 -0
- package/dist/public/assets/sequenceDiagram-PO4LG4MO-D5E8ISM1.js +169 -0
- package/dist/public/assets/sizeCapture-INFHLROL-BW3KmxDN.js +1 -0
- package/dist/public/assets/stateDiagram-v2-GCMORJYK-Bw2ZzQOE.js +291 -0
- package/dist/public/assets/swimlanes-2SLR337P-CLJ-6Ex_.js +1 -0
- package/dist/public/assets/swimlanesDiagram-TC7HE7FX-BD8rQfV7.js +8 -0
- package/dist/public/assets/timeline-definition-EJHVYXUP-C_lFJ-4W.js +120 -0
- package/dist/public/assets/usecaseDiagram-POWQR4AR-DWXawL2A.js +370 -0
- package/dist/public/assets/vennDiagram-UO4OBE2U-D4O5oZal.js +34 -0
- package/dist/public/assets/wardleyDiagram-VNRHLVJA-B6HElk__.js +78 -0
- package/dist/public/assets/xychartDiagram-PMCCYNJV-C5OAXnJy.js +7 -0
- package/dist/public/index.html +2 -2
- package/dist/src/agent-runtime/adapter.js +54 -10
- package/dist/src/agent-runtime/builtinRuntime.js +67 -6
- package/dist/src/agent-runtime/conversation.js +49 -9
- package/dist/src/agent-runtime/interaction.js +26 -12
- package/dist/src/agent-runtime/interactionLiveHub.js +4 -0
- package/dist/src/agent-runtime/runtime.js +83 -27
- package/dist/src/agent-runtime/tmuxRuntime.js +36 -6
- package/dist/src/agents/claude.js +2 -19
- package/dist/src/agents/claudeInteraction.js +2 -132
- package/dist/src/agents/claudePaneInput.js +22 -22
- package/dist/src/agents/codebuddy.js +100 -0
- package/dist/src/agents/codebuddyConversation.js +414 -0
- package/dist/src/agents/codebuddyInteraction.js +4 -0
- package/dist/src/agents/codebuddyNativeTail.js +206 -0
- package/dist/src/agents/codebuddyPaneInput.js +76 -0
- package/dist/src/agents/codebuddyTranscriptParse.js +178 -0
- package/dist/src/agents/hookEvents.js +143 -0
- package/dist/src/agents/hookInteraction.js +178 -0
- package/dist/src/agents/index.js +2 -1
- package/dist/src/claudeEvents.js +4 -5
- package/dist/src/cli/agentIntegration.js +31 -2
- package/dist/src/cli/codebuddyHooks.js +286 -0
- package/dist/src/cli/hookScaffold.js +25 -12
- package/dist/src/cli/i18n/en.js +1 -1
- package/dist/src/cli/i18n/zh.js +1 -1
- package/dist/src/cli/state.js +4 -0
- package/dist/src/cli/supervisor.js +2 -1
- package/dist/src/codebuddyEvents.js +151 -0
- package/dist/src/docs.js +21 -7
- package/dist/src/logDetail.js +27 -0
- package/dist/src/paneInput.js +107 -10
- package/dist/src/pendingPrompt.js +21 -5
- package/dist/src/routes/files.js +4 -1
- package/dist/src/routes/system.js +12 -2
- package/dist/src/server.js +69 -8
- package/dist/src/trimCapture.js +1 -1
- package/dist/src/workspace/checkpointer.js +48 -4
- package/dist/src/workspace/lock.js +54 -10
- package/dist/src/workspace/runtime.js +23 -8
- package/dist/src/workspaceProtocol.js +1 -0
- package/package.json +4 -4
- package/dist/public/assets/index-2ghkmGbT.js +0 -356
- package/dist/public/assets/index-BjnnFTLC.css +0 -32
|
@@ -3,6 +3,8 @@ import { createClaudeConversationAdapter } from '../agents/claudeConversation.js
|
|
|
3
3
|
import { createClaudeInteractionAdapter } from '../agents/claudeInteraction.js';
|
|
4
4
|
import { NativeInboxCoordinator } from '../agents/nativeInbox.js';
|
|
5
5
|
import { createCodexNativeInboxSource, } from '../agents/nativeInboxSources.js';
|
|
6
|
+
import { createCodeBuddyConversationAdapter } from '../agents/codebuddyConversation.js';
|
|
7
|
+
import { createCodeBuddyInteractionAdapter } from '../agents/codebuddyInteraction.js';
|
|
6
8
|
import { createCodexConversationAdapter } from '../agents/codexConversation.js';
|
|
7
9
|
import { createCodexInteractionAdapter } from '../agents/codexInteraction.js';
|
|
8
10
|
import { createPiConversationActivityReader, createPiConversationAdapter, } from '../agents/piConversation.js';
|
|
@@ -16,6 +18,7 @@ import { createCodexConversationActivationController } from '../agents/codexConv
|
|
|
16
18
|
import { createClaudeSubscriptionUsageAdapter } from '../agents/claudeSubscriptionUsage.js';
|
|
17
19
|
import { createCodexSubscriptionUsageAdapter } from '../agents/codexSubscriptionUsage.js';
|
|
18
20
|
import { BridgeInboxCoordinator, PiInboxBridgeCoordinator, } from '../agents/piInboxBridge.js';
|
|
21
|
+
import { hookEventSequence } from '../agents/hookEvents.js';
|
|
19
22
|
import { AgentRuntime, } from './runtime.js';
|
|
20
23
|
export function createClaudeConversationActivityReader(events) {
|
|
21
24
|
return {
|
|
@@ -40,6 +43,29 @@ export function createClaudeConversationActivityReader(events) {
|
|
|
40
43
|
},
|
|
41
44
|
};
|
|
42
45
|
}
|
|
46
|
+
// The same table Claude's reader uses, from CodeBuddy's own classification: the wave the 对话 lens draws
|
|
47
|
+
// and the roster row come from one source, so they cannot disagree about what the pane is doing.
|
|
48
|
+
export function createCodebuddyConversationActivityReader(events) {
|
|
49
|
+
return {
|
|
50
|
+
async read(run) {
|
|
51
|
+
if (events?.paneSession && events.paneSession(run.ref.paneId)?.sessionId !== run.ref.sessionId) {
|
|
52
|
+
return { activity: 'unknown', activeTurn: { state: 'unknown' } };
|
|
53
|
+
}
|
|
54
|
+
const kind = events?.paneKind?.(run.ref.paneId, run.process) ?? null;
|
|
55
|
+
const activity = kind === 'working' ? 'working'
|
|
56
|
+
: kind === 'permission' ? 'waiting'
|
|
57
|
+
: kind === 'compacting' ? 'compacting'
|
|
58
|
+
: kind === 'done' || kind === 'idle' || kind === 'end' || kind === 'error'
|
|
59
|
+
? 'idle' : 'unknown';
|
|
60
|
+
return {
|
|
61
|
+
activity,
|
|
62
|
+
activeTurn: activity === 'idle' ? { state: 'none' }
|
|
63
|
+
: activity === 'unknown' ? { state: 'unknown' }
|
|
64
|
+
: { state: 'active', nativeTurnId: `codebuddy-run:${run.ref.runId}` },
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
43
69
|
export function createCodexConversationActivityReader(app) {
|
|
44
70
|
return {
|
|
45
71
|
async read(run) {
|
|
@@ -80,7 +106,7 @@ export function createCodexConversationActivityReader(app) {
|
|
|
80
106
|
}
|
|
81
107
|
// Built-in wiring is deliberately static and product-owned. This is not a plugin loader: Agent identity
|
|
82
108
|
// still comes only from BUILTIN_AGENT_ADAPTERS, while factories close over local provider clients/Bridge.
|
|
83
|
-
export function createBuiltinAgentRuntime({ home, claudeEvents, claudeProjectsRoot, claudeConversationControl, claudeInteractionControl, codexApp, codexClear, codexActivationCommands, codexActivationReceipts, codexSessionsRoot, piSessionsRoot, ...runtimeOptions }) {
|
|
109
|
+
export function createBuiltinAgentRuntime({ home, codebuddyEvents, codebuddyConversationControl, codebuddyInteractionControl, claudeEvents, claudeProjectsRoot, claudeConversationControl, claudeInteractionControl, codexApp, codexClear, codexActivationCommands, codexActivationReceipts, codexSessionsRoot, piSessionsRoot, ...runtimeOptions }) {
|
|
84
110
|
const factories = {
|
|
85
111
|
claude: (context) => {
|
|
86
112
|
const subscriptionUsage = createClaudeSubscriptionUsageAdapter({
|
|
@@ -92,11 +118,9 @@ export function createBuiltinAgentRuntime({ home, claudeEvents, claudeProjectsRo
|
|
|
92
118
|
agentId: 'claude',
|
|
93
119
|
sourceId: 'claude.hooks',
|
|
94
120
|
label: 'Claude',
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return Number.isSafeInteger(sequence) ? sequence : null;
|
|
99
|
-
},
|
|
121
|
+
// The Hook spool's event ID carries its durable source sequence; the interpreter is the SHARED one
|
|
122
|
+
// because CodeBuddy's spool is written by the same script with the same ID shape.
|
|
123
|
+
sourceEventSequence: hookEventSequence,
|
|
100
124
|
});
|
|
101
125
|
const conversationSessions = claudeEvents && typeof claudeEvents.paneSession === 'function'
|
|
102
126
|
? claudeEvents
|
|
@@ -146,6 +170,43 @@ export function createBuiltinAgentRuntime({ home, claudeEvents, claudeProjectsRo
|
|
|
146
170
|
onBridgeConnected: (lease) => inbox.bind(lease).then(() => undefined),
|
|
147
171
|
};
|
|
148
172
|
},
|
|
173
|
+
// CodeBuddy is Claude's twin on this path: the same file Hooks, the same shared writer, the same Bridge
|
|
174
|
+
// durable lane, so it reuses the same coordinator. There is deliberately no native/legacy source — the
|
|
175
|
+
// Connector publishes both the neutral pane baseline and every lifecycle edge, and the ordered projector
|
|
176
|
+
// is the only authority for CodeBuddy state from day one.
|
|
177
|
+
codebuddy: (context) => {
|
|
178
|
+
const inbox = new BridgeInboxCoordinator({
|
|
179
|
+
host: context.bridge,
|
|
180
|
+
projector: context.inbox,
|
|
181
|
+
agentId: 'codebuddy',
|
|
182
|
+
sourceId: 'codebuddy.hooks',
|
|
183
|
+
label: 'CodeBuddy',
|
|
184
|
+
sourceEventSequence: hookEventSequence,
|
|
185
|
+
});
|
|
186
|
+
const conversationSessions = codebuddyEvents && typeof codebuddyEvents.paneSession === 'function'
|
|
187
|
+
? codebuddyEvents
|
|
188
|
+
: undefined;
|
|
189
|
+
return {
|
|
190
|
+
inbox: true,
|
|
191
|
+
conversation: createCodeBuddyConversationAdapter({
|
|
192
|
+
...(conversationSessions === undefined ? {} : { sessions: conversationSessions }),
|
|
193
|
+
...(codebuddyConversationControl === undefined ? {} : { control: codebuddyConversationControl }),
|
|
194
|
+
}),
|
|
195
|
+
conversationActivity: createCodebuddyConversationActivityReader(codebuddyEvents),
|
|
196
|
+
...(codebuddyInteractionControl === undefined ? {} : {
|
|
197
|
+
interaction: createCodeBuddyInteractionAdapter(codebuddyInteractionControl, 750, (availability, message) => context.health.report({
|
|
198
|
+
capability: 'interaction', availability,
|
|
199
|
+
...(message ? { message } : {}),
|
|
200
|
+
...(availability === 'ready' ? { lastSuccessAt: Date.now() } : {}),
|
|
201
|
+
})),
|
|
202
|
+
}),
|
|
203
|
+
start: () => {
|
|
204
|
+
inbox.start();
|
|
205
|
+
return () => inbox.close();
|
|
206
|
+
},
|
|
207
|
+
onBridgeConnected: (lease) => inbox.bind(lease).then(() => undefined),
|
|
208
|
+
};
|
|
209
|
+
},
|
|
149
210
|
codex: (context) => {
|
|
150
211
|
const resolvedCodexSessionsRoot = codexSessionsRoot ?? codexSessionsDir(home);
|
|
151
212
|
const subscriptionUsage = createCodexSubscriptionUsageAdapter({
|
|
@@ -179,6 +179,8 @@ function capabilities(raw) {
|
|
|
179
179
|
return null;
|
|
180
180
|
if (raw.steer !== undefined && raw.steer !== true)
|
|
181
181
|
return null;
|
|
182
|
+
if (raw.promptWhileActive !== undefined && raw.promptWhileActive !== true)
|
|
183
|
+
return null;
|
|
182
184
|
if (raw.branching !== undefined && raw.branching !== true)
|
|
183
185
|
return null;
|
|
184
186
|
return {
|
|
@@ -186,6 +188,7 @@ function capabilities(raw) {
|
|
|
186
188
|
live: raw.live,
|
|
187
189
|
...(raw.sendable === true ? { sendable: true } : {}),
|
|
188
190
|
...(raw.steer === true ? { steer: true } : {}),
|
|
191
|
+
...(raw.promptWhileActive === true ? { promptWhileActive: true } : {}),
|
|
189
192
|
...(raw.send === undefined ? {} : {
|
|
190
193
|
send: [...raw.send],
|
|
191
194
|
}),
|
|
@@ -579,6 +582,9 @@ export class ConversationService {
|
|
|
579
582
|
};
|
|
580
583
|
}
|
|
581
584
|
const activity = await this.#activitySource.read(run);
|
|
585
|
+
// Probed before the transaction: whether this provider's own input takes a prompt while a turn runs is
|
|
586
|
+
// a provider capability (the descriptor), while the decision below is state.
|
|
587
|
+
const promptWhileActive = await this.#promptWhileActive(adapter, run);
|
|
582
588
|
const begin = await this.#withWrite(() => {
|
|
583
589
|
const existing = this.#submission(session.agentId, session.sessionId, request.clientRequestId);
|
|
584
590
|
if (existing) {
|
|
@@ -602,8 +608,15 @@ export class ConversationService {
|
|
|
602
608
|
}
|
|
603
609
|
const hasQueue = this.#state.submissions.some((item) => item.agentId === session.agentId
|
|
604
610
|
&& item.sessionId === session.sessionId && item.state === 'queued');
|
|
605
|
-
|
|
606
|
-
|
|
611
|
+
// A provider whose TUI queues the text natively can be written to WHILE its turn runs, so a phone
|
|
612
|
+
// send does not have to wait for idle — the user's message joins the turn in progress, exactly as if
|
|
613
|
+
// they had typed it in the terminal. Only a running turn qualifies: a permission gate, a compaction
|
|
614
|
+
// or an unknown state is not an input the pane will take (see ConversationCapabilities).
|
|
615
|
+
const inTurn = promptWhileActive && !hasQueue
|
|
616
|
+
&& activity.activity === 'working' && activity.activeTurn.state === 'active'
|
|
617
|
+
&& cycle.state !== 'closed';
|
|
618
|
+
const direct = inTurn || (activity.activity === 'idle' && activity.activeTurn.state === 'none'
|
|
619
|
+
&& cycle.state === 'closed' && !hasQueue);
|
|
607
620
|
const revision = ++this.#state.ledgerRevision;
|
|
608
621
|
const submission = {
|
|
609
622
|
agentId: session.agentId, sessionId: session.sessionId,
|
|
@@ -616,7 +629,9 @@ export class ConversationService {
|
|
|
616
629
|
...this.#currentBaseline(session.agentId, session.sessionId),
|
|
617
630
|
};
|
|
618
631
|
this.#state.submissions.push(submission);
|
|
619
|
-
|
|
632
|
+
// An in-turn send joins a turn that is ALREADY claimed: claiming (or reopening) the cycle here would
|
|
633
|
+
// tell the Core a new turn started, and the running turn's completion would then never be observed.
|
|
634
|
+
if (direct && !inTurn)
|
|
620
635
|
this.#claimCycle(cycle, submission, activity);
|
|
621
636
|
try {
|
|
622
637
|
this.#save();
|
|
@@ -628,7 +643,7 @@ export class ConversationService {
|
|
|
628
643
|
} };
|
|
629
644
|
}
|
|
630
645
|
return direct
|
|
631
|
-
? { kind: 'dispatch', id: submission.clientRequestId }
|
|
646
|
+
? { kind: 'dispatch', id: submission.clientRequestId, inTurn }
|
|
632
647
|
: { kind: 'receipt', value: this.#submitReceipt(submission) };
|
|
633
648
|
});
|
|
634
649
|
if (begin.kind === 'receipt') {
|
|
@@ -636,7 +651,7 @@ export class ConversationService {
|
|
|
636
651
|
this.#wake(owner);
|
|
637
652
|
return begin.value;
|
|
638
653
|
}
|
|
639
|
-
return this.#dispatchClaim(run, begin.id, 'direct');
|
|
654
|
+
return this.#dispatchClaim(run, begin.id, 'direct', undefined, begin.inTurn);
|
|
640
655
|
}
|
|
641
656
|
async queueSnapshot(run) {
|
|
642
657
|
this.#requireRun(run);
|
|
@@ -1487,7 +1502,11 @@ export class ConversationService {
|
|
|
1487
1502
|
}
|
|
1488
1503
|
this.#state.submissions = this.#state.submissions.filter((candidate) => candidate !== item);
|
|
1489
1504
|
}
|
|
1490
|
-
async #dispatchClaim(run, submissionId, origin, detached
|
|
1505
|
+
async #dispatchClaim(run, submissionId, origin, detached,
|
|
1506
|
+
// This dispatch was admitted knowing a turn is running, because the provider's own input takes a prompt
|
|
1507
|
+
// while it works (ConversationCapabilities.promptWhileActive). Only send() passes it: a queued delivery
|
|
1508
|
+
// still waits for idle, and that is what keeps the queue's ordering promise intact.
|
|
1509
|
+
promptWhileActive = false) {
|
|
1491
1510
|
const runRef = copyRun(run.ref);
|
|
1492
1511
|
if (!runRef.sessionId) {
|
|
1493
1512
|
return { status: 'rejected', reason: 'invalid_request', nativeMutation: false };
|
|
@@ -1512,8 +1531,9 @@ export class ConversationService {
|
|
|
1512
1531
|
|| preDispatchActivity.activeTurn.state !== 'none')))) {
|
|
1513
1532
|
receipt = { outcome: 'rejected', nativeMutation: false, reason: 'conflict' };
|
|
1514
1533
|
}
|
|
1515
|
-
else if (origin !== 'steer' && (preDispatchActivity.activity !== 'idle'
|
|
1534
|
+
else if (!promptWhileActive && origin !== 'steer' && (preDispatchActivity.activity !== 'idle'
|
|
1516
1535
|
|| preDispatchActivity.activeTurn.state !== 'none')) {
|
|
1536
|
+
// A busy provider that has NOT declared in-turn input gets the message queued, never written to.
|
|
1517
1537
|
receipt = { outcome: 'busy', nativeMutation: false };
|
|
1518
1538
|
}
|
|
1519
1539
|
else
|
|
@@ -1538,8 +1558,12 @@ export class ConversationService {
|
|
|
1538
1558
|
return !run.signal.aborted && this.#runs.resolve(runRef) === run
|
|
1539
1559
|
&& current.epoch === preDispatchActivity.epoch
|
|
1540
1560
|
&& current.revision === preDispatchActivity.revision
|
|
1541
|
-
|
|
1542
|
-
|
|
1561
|
+
// Every dispatch requires the world it was decided in. The idle path additionally requires
|
|
1562
|
+
// the pane to STILL be idle; the in-turn path was decided knowing a turn is running, so it
|
|
1563
|
+
// requires only that nothing has moved — if it has, the adapter writes nothing and the
|
|
1564
|
+
// message falls back to the queue.
|
|
1565
|
+
&& (promptWhileActive
|
|
1566
|
+
|| (current.activity === 'idle' && current.activeTurn.state === 'none'));
|
|
1543
1567
|
},
|
|
1544
1568
|
});
|
|
1545
1569
|
if (!operation)
|
|
@@ -2102,6 +2126,22 @@ export class ConversationService {
|
|
|
2102
2126
|
if (changed)
|
|
2103
2127
|
this.#wake(owner);
|
|
2104
2128
|
}
|
|
2129
|
+
// Provider capability: does its own input accept a prompt while a turn is running? Read from the
|
|
2130
|
+
// descriptor, the same place every other provider capability comes from, and never fatal — an
|
|
2131
|
+
// unavailable or mismatched probe simply means "no", leaving the ordinary queue path in charge.
|
|
2132
|
+
async #promptWhileActive(adapter, run) {
|
|
2133
|
+
try {
|
|
2134
|
+
const raw = await adapter.discoverNative(run.ref);
|
|
2135
|
+
if (raw === null)
|
|
2136
|
+
return false;
|
|
2137
|
+
const descriptor = this.#descriptor(raw, run.ref.agentId);
|
|
2138
|
+
return descriptor.session.sessionId === run.ref.sessionId
|
|
2139
|
+
&& descriptor.capabilities.promptWhileActive === true;
|
|
2140
|
+
}
|
|
2141
|
+
catch {
|
|
2142
|
+
return false;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2105
2145
|
#wake(owner) {
|
|
2106
2146
|
const state = this.#dispatchers.get(owner) ?? { running: false, wakePending: false, timer: undefined };
|
|
2107
2147
|
this.#dispatchers.set(owner, state);
|
|
@@ -343,17 +343,16 @@ export class InteractionService {
|
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
345
|
for (const item of baseline) {
|
|
346
|
-
let record = this.#
|
|
346
|
+
let record = this.#pendingRecordBySource(run.ref, item.id);
|
|
347
347
|
if (!record) {
|
|
348
348
|
record = this.#createRecord(run.ref, item);
|
|
349
349
|
this.#state.interactions.push(record);
|
|
350
350
|
}
|
|
351
|
-
else
|
|
351
|
+
else {
|
|
352
352
|
record.pending = item;
|
|
353
353
|
record.updatedAt = this.#now();
|
|
354
354
|
}
|
|
355
|
-
|
|
356
|
-
state.pendingBySource.set(item.id, record);
|
|
355
|
+
state.pendingBySource.set(item.id, record);
|
|
357
356
|
}
|
|
358
357
|
this.#save();
|
|
359
358
|
});
|
|
@@ -496,8 +495,14 @@ export class InteractionService {
|
|
|
496
495
|
#recordsFor(run) {
|
|
497
496
|
return this.#state.interactions.filter((record) => (record.agentId === run.agentId && record.run.runId === run.runId));
|
|
498
497
|
}
|
|
499
|
-
|
|
500
|
-
|
|
498
|
+
// A native id names the SHAPE of the gate its adapter reads off the screen (see hookInteraction), so the
|
|
499
|
+
// same id legitimately comes back for a new occurrence: every AskUserQuestion review screen parses to
|
|
500
|
+
// "Ready to submit your answers?" with the same two options. Only a still-pending record IS that item —
|
|
501
|
+
// a resolved one belongs to an occurrence that is over, and re-publishing must mint a new public identity
|
|
502
|
+
// for the new one instead of silently dropping it (a dropped re-appearance left the phone with no card at
|
|
503
|
+
// all while the pane sat on the review screen).
|
|
504
|
+
#pendingRecordBySource(run, sourceId) {
|
|
505
|
+
return this.#recordsFor(run).find((record) => (record.sourceInteractionId === sourceId && record.state === 'pending'));
|
|
501
506
|
}
|
|
502
507
|
#accept(state, event) {
|
|
503
508
|
if (state.phase === 'closed')
|
|
@@ -567,10 +572,7 @@ export class InteractionService {
|
|
|
567
572
|
const item = pending(event.interaction);
|
|
568
573
|
if (!item)
|
|
569
574
|
throw new InteractionContractError('Interaction opened payload is invalid');
|
|
570
|
-
let record = this.#
|
|
571
|
-
if (record && record.state !== 'pending') {
|
|
572
|
-
throw new InteractionContractError('Resolved native interaction id was reused');
|
|
573
|
-
}
|
|
575
|
+
let record = this.#pendingRecordBySource(state.run.ref, item.id);
|
|
574
576
|
await this.#withWrite(() => {
|
|
575
577
|
if (!record) {
|
|
576
578
|
record = this.#createRecord(state.run.ref, item);
|
|
@@ -596,8 +598,16 @@ export class InteractionService {
|
|
|
596
598
|
throw new InteractionContractError('Interaction terminal payload is invalid');
|
|
597
599
|
}
|
|
598
600
|
const record = state.pendingBySource.get(event.interactionId);
|
|
599
|
-
if (!record)
|
|
601
|
+
if (!record) {
|
|
602
|
+
// A dispatched answer already retired this item, so the adapter reporting that the gate left the screen
|
|
603
|
+
// is the same fact arriving a second time — and an adapter only stops reporting it once its next poll
|
|
604
|
+
// sees a different screen. Failing the observation closed here killed every later card with it: the
|
|
605
|
+
// phone was left with no card while the pane sat on the next gate (the review screen after a question).
|
|
606
|
+
const settled = this.#recordsFor(state.run.ref).some((candidate) => (candidate.sourceInteractionId === event.interactionId && candidate.state !== 'pending'));
|
|
607
|
+
if (settled)
|
|
608
|
+
return;
|
|
600
609
|
throw new InteractionContractError('Interaction terminal event targets unknown pending item');
|
|
610
|
+
}
|
|
601
611
|
await this.#withWrite(() => {
|
|
602
612
|
record.state = 'resolved';
|
|
603
613
|
record.receipt = event.type === 'resolved'
|
|
@@ -658,7 +668,11 @@ export class InteractionService {
|
|
|
658
668
|
});
|
|
659
669
|
if (!finalized || !live || !live.exposed || live.phase === 'closed')
|
|
660
670
|
return;
|
|
661
|
-
|
|
671
|
+
// The index is keyed by native id, and a new occurrence of the same gate may already have taken that
|
|
672
|
+
// key over — only retire the entry this response actually answered.
|
|
673
|
+
if (live.pendingBySource.get(record.sourceInteractionId) === record) {
|
|
674
|
+
live.pendingBySource.delete(record.sourceInteractionId);
|
|
675
|
+
}
|
|
662
676
|
if (result.status === 'accepted' || result.status === 'already_resolved') {
|
|
663
677
|
await this.#emit(live, {
|
|
664
678
|
type: 'resolved', revision: ++live.revision, interactionId: record.publicInteractionId,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
const DEFAULT_MAX_BUFFERED_EVENTS = 256;
|
|
2
3
|
const DEFAULT_IDLE_GRACE_MS = 5_000;
|
|
3
4
|
export class InteractionLiveHubError extends Error {
|
|
@@ -128,6 +129,7 @@ export class InteractionLiveHub {
|
|
|
128
129
|
#create(run) {
|
|
129
130
|
const shared = {
|
|
130
131
|
run,
|
|
132
|
+
epoch: randomUUID(),
|
|
131
133
|
ready: Promise.resolve(),
|
|
132
134
|
subscribers: new Set(),
|
|
133
135
|
idleTimer: undefined,
|
|
@@ -144,6 +146,7 @@ export class InteractionLiveHub {
|
|
|
144
146
|
}
|
|
145
147
|
shared.handle = handle;
|
|
146
148
|
shared.checkpoint = {
|
|
149
|
+
epoch: shared.epoch,
|
|
147
150
|
revision: handle.revision,
|
|
148
151
|
pending: structuredClone(handle.pending),
|
|
149
152
|
};
|
|
@@ -181,6 +184,7 @@ export class InteractionLiveHub {
|
|
|
181
184
|
throw new InteractionLiveHubError('unavailable', 'Interaction terminal event is invalid');
|
|
182
185
|
}
|
|
183
186
|
shared.checkpoint = {
|
|
187
|
+
epoch: shared.epoch,
|
|
184
188
|
revision: event.revision,
|
|
185
189
|
pending: [...pending.values()],
|
|
186
190
|
};
|
|
@@ -542,16 +542,30 @@ export class AgentRuntime {
|
|
|
542
542
|
.sort((first, second) => first.paneId.localeCompare(second.paneId)
|
|
543
543
|
|| first.agentId.localeCompare(second.agentId));
|
|
544
544
|
}
|
|
545
|
+
// Per-pane process evidence for a single identity decision. Both probes are lazily memoized so a decision
|
|
546
|
+
// that only needs one of them costs one `ps` read, and every evidence method the host offers must be
|
|
547
|
+
// forwarded here or adapters silently lose it.
|
|
548
|
+
#processContext(pane) {
|
|
549
|
+
const source = this.#process;
|
|
550
|
+
let inspected;
|
|
551
|
+
let group;
|
|
552
|
+
return {
|
|
553
|
+
inspectForeground: () => {
|
|
554
|
+
inspected ??= source.inspectForeground(pane);
|
|
555
|
+
return inspected;
|
|
556
|
+
},
|
|
557
|
+
...(source.inspectForegroundGroup === undefined ? {} : {
|
|
558
|
+
inspectForegroundGroup: () => {
|
|
559
|
+
group ??= source.inspectForegroundGroup(pane);
|
|
560
|
+
return group;
|
|
561
|
+
},
|
|
562
|
+
}),
|
|
563
|
+
};
|
|
564
|
+
}
|
|
545
565
|
async identifyPanes(panes) {
|
|
546
566
|
const activeAdapters = this.adapters.filter((adapter) => (!this.#lifecycles.get(adapter.id)?.abort.signal.aborted));
|
|
547
567
|
const resolved = await Promise.all(panes.map(async (pane) => {
|
|
548
|
-
|
|
549
|
-
const context = {
|
|
550
|
-
inspectForeground: () => {
|
|
551
|
-
inspected ??= this.#process.inspectForeground(pane);
|
|
552
|
-
return inspected;
|
|
553
|
-
},
|
|
554
|
-
};
|
|
568
|
+
const context = this.#processContext(pane);
|
|
555
569
|
const identity = await resolveAgentIdentity(pane, activeAdapters, context, {
|
|
556
570
|
verifyTimeoutMs: this.#verifyTimeoutMs,
|
|
557
571
|
});
|
|
@@ -905,16 +919,63 @@ export class AgentRuntime {
|
|
|
905
919
|
return false;
|
|
906
920
|
}
|
|
907
921
|
const pane = panes.find((value) => value.paneId === candidate.paneId);
|
|
908
|
-
|
|
922
|
+
if (!pane)
|
|
923
|
+
return false;
|
|
924
|
+
if (await this.#anchorStillOwnsPane(candidate, pane))
|
|
925
|
+
return true;
|
|
926
|
+
return (await this.#verifyAgainst(adapter, candidate, pane)) === 'valid';
|
|
927
|
+
}
|
|
928
|
+
// The hook that produced this candidate already named the process that owns the pane (pid, start time and
|
|
929
|
+
// tty). While that exact generation is alive on the pane's own tty the pane cannot have changed under it, and
|
|
930
|
+
// re-deriving the identity by scanning the pane's foreground group is both slower and flakier: while the
|
|
931
|
+
// Agent runs a tool, the group's foreground owner is that tool, so the scan intermittently finds no Agent and
|
|
932
|
+
// the attachment is refused — which is what makes an Agent's row blink in and out of the roster, each
|
|
933
|
+
// refusal revoking the run and the next attempt creating a new one. Anything else — the pid gone, a
|
|
934
|
+
// different start time, a tty we cannot tie to this pane, no probe on this host, a probe that threw — falls
|
|
935
|
+
// through to the full verification, so this only ever skips work.
|
|
936
|
+
async #anchorStillOwnsPane(candidate, pane) {
|
|
937
|
+
const anchor = candidate.process;
|
|
938
|
+
const inspectProcess = this.#process.inspectProcess;
|
|
939
|
+
if (!inspectProcess || !anchor || anchor.startedAt === undefined)
|
|
940
|
+
return false;
|
|
941
|
+
try {
|
|
942
|
+
const alive = await inspectProcess(anchor.pid);
|
|
943
|
+
if (alive === null || alive.startedAt !== anchor.startedAt)
|
|
944
|
+
return false;
|
|
945
|
+
// A recycled pid on another pane is not this attachment.
|
|
946
|
+
const tty = alive.tty ?? anchor.tty;
|
|
947
|
+
return !!tty && !!pane.tty && tty === pane.tty;
|
|
948
|
+
}
|
|
949
|
+
catch {
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
// Steady-state recheck for a lease that already exists. The lease carries the process generation it was
|
|
954
|
+
// attached to — pid AND start time — and while that exact generation is alive the pane cannot have changed
|
|
955
|
+
// under it, so the full identity probe (a `ps -t` plus the lsof that resolves an executable, ~100ms for a
|
|
956
|
+
// Node pane) would only re-derive what the anchor already proves. Confirm the anchor with the one cheap
|
|
957
|
+
// probe instead. Anything else — the pid gone, a different start time, no probe on this host, a probe that
|
|
958
|
+
// threw — falls through to the full verification, so this can only ever skip work, never revoke on doubt.
|
|
959
|
+
async #verifyTracked(tracked, pane) {
|
|
960
|
+
const anchor = tracked.candidate.process;
|
|
961
|
+
const inspectProcess = this.#process.inspectProcess;
|
|
962
|
+
if (inspectProcess) {
|
|
963
|
+
try {
|
|
964
|
+
const alive = await inspectProcess(anchor.pid);
|
|
965
|
+
// Alive with the start time we anchored on — or alive with no start time to compare, which the hook
|
|
966
|
+
// sometimes cannot read: a live anchor is not doubt. Falling through to the full verification would
|
|
967
|
+
// scan the pane's foreground group, find the TOOL the Agent is running rather than the Agent, and
|
|
968
|
+
// revoke a run that is plainly still there.
|
|
969
|
+
if (alive !== null && (anchor.startedAt === undefined || alive.startedAt === anchor.startedAt)) {
|
|
970
|
+
return 'valid';
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
catch { /* fall through to the full verification */ }
|
|
974
|
+
}
|
|
975
|
+
return this.#verifyAgainst(tracked.adapter, tracked.candidate, pane);
|
|
909
976
|
}
|
|
910
977
|
async #verifyAgainst(adapter, candidate, pane) {
|
|
911
|
-
|
|
912
|
-
const context = {
|
|
913
|
-
inspectForeground: () => {
|
|
914
|
-
inspected ??= this.#process.inspectForeground(pane);
|
|
915
|
-
return inspected;
|
|
916
|
-
},
|
|
917
|
-
};
|
|
978
|
+
const context = this.#processContext(pane);
|
|
918
979
|
try {
|
|
919
980
|
return await lifecycleWithin((async () => {
|
|
920
981
|
const identity = await resolveAgentIdentity(pane, this.adapters, context, {
|
|
@@ -924,7 +985,9 @@ export class AgentRuntime {
|
|
|
924
985
|
return 'unknown';
|
|
925
986
|
if (identity.kind !== 'matched' || identity.adapter.id !== adapter.id)
|
|
926
987
|
return 'invalid';
|
|
927
|
-
|
|
988
|
+
// An adapter that names its own process anchors the lease on it. The foreground leaf is only the
|
|
989
|
+
// default, for the Agents that are themselves the pane's foreground process.
|
|
990
|
+
const foreground = identity.process ?? await context.inspectForeground(pane);
|
|
928
991
|
if (foreground === null)
|
|
929
992
|
return 'unknown';
|
|
930
993
|
// A partial best-effort probe cannot prove that the process generation changed. Preserve the
|
|
@@ -944,9 +1007,7 @@ export class AgentRuntime {
|
|
|
944
1007
|
if (tracked.lease.signal.aborted || this.runs.resolve(tracked.lease.ref) !== tracked.lease)
|
|
945
1008
|
continue;
|
|
946
1009
|
const pane = panes.get(tracked.lease.ref.paneId);
|
|
947
|
-
const verdict = pane
|
|
948
|
-
? await this.#verifyAgainst(tracked.adapter, tracked.candidate, pane)
|
|
949
|
-
: 'invalid';
|
|
1010
|
+
const verdict = pane ? await this.#verifyTracked(tracked, pane) : 'invalid';
|
|
950
1011
|
if (verdict === 'invalid' && this.runs.resolve(tracked.lease.ref) === tracked.lease) {
|
|
951
1012
|
await this.runs.revokePane(tracked.lease.ref.paneId, pane ? 'process_exit' : 'pane_detached');
|
|
952
1013
|
}
|
|
@@ -955,13 +1016,7 @@ export class AgentRuntime {
|
|
|
955
1016
|
for (const pane of panes.values()) {
|
|
956
1017
|
if (this.#closed || this.runs.currentForPane(pane.paneId))
|
|
957
1018
|
continue;
|
|
958
|
-
|
|
959
|
-
const context = {
|
|
960
|
-
inspectForeground: () => {
|
|
961
|
-
inspected ??= this.#process.inspectForeground(pane);
|
|
962
|
-
return inspected;
|
|
963
|
-
},
|
|
964
|
-
};
|
|
1019
|
+
const context = this.#processContext(pane);
|
|
965
1020
|
const identity = await resolveAgentIdentity(pane, activeAdapters, context, {
|
|
966
1021
|
verifyTimeoutMs: this.#verifyTimeoutMs,
|
|
967
1022
|
});
|
|
@@ -971,7 +1026,8 @@ export class AgentRuntime {
|
|
|
971
1026
|
try {
|
|
972
1027
|
// Exact command matches skip Adapter verification, so bound this probe at its own use site.
|
|
973
1028
|
// A timed-out result must not attach later or block other panes and subsequent snapshots.
|
|
974
|
-
|
|
1029
|
+
// A verifier-named process was already resolved inside the verification budget.
|
|
1030
|
+
foreground = identity.process ?? await lifecycleWithin(context.inspectForeground(pane), this.#verifyTimeoutMs, 'Agent process discovery');
|
|
975
1031
|
}
|
|
976
1032
|
catch {
|
|
977
1033
|
continue;
|
|
@@ -150,16 +150,24 @@ export async function processStartedAt(run, pid) {
|
|
|
150
150
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
151
151
|
}
|
|
152
152
|
export function createLocalAgentProcessContext({ run = defaultRun, } = {}) {
|
|
153
|
+
const PS_FORMAT = 'pid=,ppid=,stat=,etime=,tty=,command=';
|
|
154
|
+
// One `ps` read, shared by both evidence methods below. The full-system scan is only the fallback for a
|
|
155
|
+
// platform whose targeted query returns nothing; rows are always re-filtered to the pane's own tty.
|
|
156
|
+
const foregroundGroup = async (pane) => {
|
|
157
|
+
const tty = normTty(pane.tty);
|
|
158
|
+
if (!tty)
|
|
159
|
+
return { tty: '', rows: [] };
|
|
160
|
+
let output = await run('ps', ['-t', tty, '-o', PS_FORMAT]);
|
|
161
|
+
if (!String(output).trim())
|
|
162
|
+
output = await run('ps', ['-Ao', PS_FORMAT]);
|
|
163
|
+
return { tty, rows: foregroundRows(output).filter((row) => row.tty === tty) };
|
|
164
|
+
};
|
|
153
165
|
return {
|
|
154
166
|
async inspectForeground(pane) {
|
|
155
|
-
const tty =
|
|
167
|
+
const { tty, rows } = await foregroundGroup(pane);
|
|
156
168
|
if (!tty)
|
|
157
169
|
return null;
|
|
158
|
-
|
|
159
|
-
if (!String(output).trim()) {
|
|
160
|
-
output = await run('ps', ['-Ao', 'pid=,ppid=,stat=,etime=,tty=,command=']);
|
|
161
|
-
}
|
|
162
|
-
const row = foregroundLeaf(foregroundRows(output), tty, pane.currentCommand);
|
|
170
|
+
const row = foregroundLeaf(rows, tty, pane.currentCommand);
|
|
163
171
|
if (!row)
|
|
164
172
|
return null;
|
|
165
173
|
const startedAt = await processStartedAt(run, row.pid);
|
|
@@ -172,5 +180,27 @@ export function createLocalAgentProcessContext({ run = defaultRun, } = {}) {
|
|
|
172
180
|
...(row.command ? { commandLine: row.command } : {}),
|
|
173
181
|
};
|
|
174
182
|
},
|
|
183
|
+
async inspectForegroundGroup(pane) {
|
|
184
|
+
const { rows } = await foregroundGroup(pane);
|
|
185
|
+
// Start time is resolved per row: a returned row can become a lease anchor, and a pid without a
|
|
186
|
+
// process generation is not an identity. Executable is deliberately NOT resolved — one lsof per row
|
|
187
|
+
// is expensive, and the adapters that need the group match on the command line.
|
|
188
|
+
return Promise.all(rows.map(async (row) => {
|
|
189
|
+
const startedAt = await processStartedAt(run, row.pid);
|
|
190
|
+
return {
|
|
191
|
+
pid: row.pid,
|
|
192
|
+
ppid: row.ppid,
|
|
193
|
+
tty: pane.tty ?? `/dev/${row.tty}`,
|
|
194
|
+
...(startedAt === undefined ? {} : { startedAt }),
|
|
195
|
+
...(row.command ? { commandLine: row.command } : {}),
|
|
196
|
+
};
|
|
197
|
+
}));
|
|
198
|
+
},
|
|
199
|
+
// One `ps -p`, and deliberately no executable lookup: a lease only needs to know that the pid it was
|
|
200
|
+
// attached to is still the same process generation.
|
|
201
|
+
async inspectProcess(pid) {
|
|
202
|
+
const startedAt = await processStartedAt(run, pid);
|
|
203
|
+
return startedAt === undefined ? null : { pid, startedAt };
|
|
204
|
+
},
|
|
175
205
|
};
|
|
176
206
|
}
|
|
@@ -13,6 +13,7 @@ import os from 'node:os';
|
|
|
13
13
|
import { resolveEncodedDirSession, isSessionUuid } from './scanUtils.js';
|
|
14
14
|
import { resolveByExecutable, executableBasename } from './processIdentity.js';
|
|
15
15
|
import { createTranscriptParser, parseTranscript } from '../transcriptParse.js';
|
|
16
|
+
import { hookErrorMessage } from './hookEvents.js';
|
|
16
17
|
const record = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
17
18
|
? value
|
|
18
19
|
: null);
|
|
@@ -59,24 +60,6 @@ function permMsg(body) {
|
|
|
59
60
|
return '需要你批准计划';
|
|
60
61
|
return t ? `需要你授权:${t}` : '需要你';
|
|
61
62
|
}
|
|
62
|
-
// Friendly Chinese for the StopFailure error type (matcher values, see the hooks doc). The payload shape
|
|
63
|
-
// isn't verified against a live rate-limit yet, so read the type defensively from several likely fields and
|
|
64
|
-
// always fall back to a bare 本轮出错 — a wrong field name degrades to the generic label, never throws.
|
|
65
|
-
const STOPFAIL_LABEL = {
|
|
66
|
-
rate_limit: '触发限流', overloaded: '服务过载', authentication_failed: '认证失败',
|
|
67
|
-
oauth_org_not_allowed: '组织未授权', billing_error: '额度/账单问题', invalid_request: '请求无效',
|
|
68
|
-
model_not_found: '模型不可用', server_error: '服务端错误', max_output_tokens: '输出超长', unknown: '未知错误',
|
|
69
|
-
};
|
|
70
|
-
function stopFailMsg(body) {
|
|
71
|
-
const error = record(body.error);
|
|
72
|
-
const type = text(body.error_type) || text(body.reason) || text(body.type)
|
|
73
|
-
|| text(body.error) || text(error?.type);
|
|
74
|
-
const label = STOPFAIL_LABEL[type];
|
|
75
|
-
if (label)
|
|
76
|
-
return label;
|
|
77
|
-
const raw = text(body.error) || text(body.message);
|
|
78
|
-
return raw ? String(raw).replace(/\s+/g, ' ').trim().slice(0, 80) : '';
|
|
79
|
-
}
|
|
80
63
|
// Build the 进行中 one-liner for a resume (PostToolUse after the user answered/approved), surfacing the
|
|
81
64
|
// choice they just made — AskUserQuestion stores it in tool_input.answers, keyed by question.
|
|
82
65
|
function resumeMsg(body) {
|
|
@@ -136,7 +119,7 @@ export function classifyClaude(src, rawBody = {}) {
|
|
|
136
119
|
if (src === 'compact')
|
|
137
120
|
return null; // PostCompact: done → clear 压缩中/进行中
|
|
138
121
|
if (src === 'stopfail')
|
|
139
|
-
return { kind: 'error', msg:
|
|
122
|
+
return { kind: 'error', msg: hookErrorMessage(body) }; // turn died on an API error
|
|
140
123
|
if (src === 'end')
|
|
141
124
|
return { kind: 'end' };
|
|
142
125
|
if (src === 'start')
|