@tianshu-ai/tianshu 0.50.3 → 0.52.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tianshu-ai/tianshu",
3
- "version": "0.50.3",
3
+ "version": "0.52.0",
4
4
  "description": "An open AI agent platform with a sidecar browser. Built in public.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/tianshu-ai/tianshu",
@@ -1 +1 @@
1
- {"version":3,"file":"ws-upgrade.d.ts","sourceRoot":"","sources":["../../src/boot/ws-upgrade.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AAQrC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAGlE,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,UAAU,CAAC;IACnB,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,EAAE,cAAc,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,wBAAwB,GAC7B,eAAe,CA4GjB"}
1
+ {"version":3,"file":"ws-upgrade.d.ts","sourceRoot":"","sources":["../../src/boot/ws-upgrade.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AAQrC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAGlE,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,UAAU,CAAC;IACnB,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,EAAE,cAAc,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,wBAAwB,GAC7B,eAAe,CAkIjB"}
@@ -25,7 +25,28 @@ import { attachChatHandler } from "../chat/handler.js";
25
25
  export function installChatWebSocket(deps) {
26
26
  const { server, globalOps, pluginRegistry } = deps;
27
27
  const wss = new WebSocketServer({ server, path: "/ws" });
28
+ // Server-side ping/pong: keep WS connections alive through NAT/
29
+ // firewalls (typically drop idle TCP after 60-120s) and detect
30
+ // dead clients. 30s interval; client has 10s to pong back.
31
+ const PING_INTERVAL_MS = 30_000;
32
+ const PONG_TIMEOUT_MS = 10_000;
33
+ const aliveSet = new Set();
34
+ const pingTimer = setInterval(() => {
35
+ for (const client of wss.clients) {
36
+ if (!aliveSet.has(client)) {
37
+ // Missed previous pong — terminate.
38
+ client.terminate();
39
+ continue;
40
+ }
41
+ aliveSet.delete(client);
42
+ client.ping();
43
+ }
44
+ }, PING_INTERVAL_MS);
45
+ pingTimer.unref();
46
+ wss.on("close", () => clearInterval(pingTimer));
28
47
  wss.on("connection", async (socket, request) => {
48
+ aliveSet.add(socket);
49
+ socket.on("pong", () => aliveSet.add(socket));
29
50
  // Build the chain from the live auth config so WS honours the same
30
51
  // login wall as HTTP. Skipping this (pinning DEV_RESOLVER_CHAIN) would
31
52
  // be an isolation hole: an unauthenticated socket could bypass auth.
@@ -1 +1 @@
1
- {"version":3,"file":"ws-upgrade.js","sourceRoot":"","sources":["../../src/boot/ws-upgrade.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ws-upgrade.js","sourceRoot":"","sources":["../../src/boot/ws-upgrade.ts"],"names":[],"mappings":"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"}
@@ -60,41 +60,6 @@ interface RunPromptArgs {
60
60
  retryLastTurn?: boolean;
61
61
  }
62
62
  export declare function runPrompt(args: RunPromptArgs): Promise<void>;
63
- /**
64
- * Build the retry prompt for the auto-recovery loop after a
65
- * transient provider error / abort.
66
- *
67
- * Bare "please retry" caused endless blind-retry loops when the
68
- * failing tool was itself the reason for the abort (e.g. a bridge
69
- * disconnected while a `bridge_*_exec` call was in flight, so the
70
- * tool_call never returned a tool_result, provider timed out, we
71
- * blindly told the agent to "retry what you were doing", the agent
72
- * re-fired the same tool call, and the whole cycle repeated).
73
- *
74
- * When we can identify the tool(s) that were being called, name
75
- * them explicitly and steer the agent toward an environment probe /
76
- * user-facing failure report instead of re-issuing the exact same
77
- * call.
78
- *
79
- * DATA SOURCES (in preference order):
80
- *
81
- * 1. `outstandingToolCalls`: a Map handler.ts maintains from
82
- * pi's `tool_execution_start` / `tool_execution_end` events.
83
- * Reliable ONLY when execution never got to _end. In practice
84
- * pi emits both events as a pair even on aborts, so this map
85
- * is almost always empty by the time we get here.
86
- *
87
- * 2. `lastAssistantMessageContent`: the full JSON body of the
88
- * just-persisted failing assistant message. It contains the
89
- * `toolCall` blocks the model was trying to invoke, so we
90
- * can name them even when the harness already flushed its
91
- * execution-end events. This is the source that actually
92
- * catches the bridge-disconnect / mid-call abort scenario
93
- * Yu reported.
94
- *
95
- * Exported so it can be unit-tested; not intended for external
96
- * callers.
97
- */
98
63
  export declare function buildAutoRecoveryPrompt(outstandingToolCalls: ReadonlyMap<string, {
99
64
  name: string;
100
65
  }>, lastAssistantMessageContent?: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/chat/handler.ts"],"names":[],"mappings":"AAgDA,OAAO,EACL,mBAAmB,EACnB,cAAc,EAEd,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC9B,MAAM,uBAAuB,CAAC;AAI/B,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,GAC9B,CAAC;AAGF,OAAO,EACL,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,2BAA2B,EAC3B,wBAAwB,EACxB,2BAA2B,EAC3B,yBAAyB,EACzB,6BAA6B,EAC7B,2BAA2B,EAC3B,4BAA4B,EAC5B,KAAK,oBAAoB,GAC1B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,EAOL,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AAmB1B,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAOL,KAAK,WAAW,EAEjB,MAAM,eAAe,CAAC;AAWvB,OAAO,EAGL,KAAK,SAAS,EAEd,KAAK,cAAc,EAEpB,MAAM,kBAAkB,CAAC;AAY1B,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,aAAa,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;IAClB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,6BAA6B,EAAE,cAAc,CAAC;IACtE,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CAyP7D;AAED,UAAU,aAAa;IACrB,GAAG,EAAE,aAAa,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,CAAC,GAAG,EAAE,SAAS,KAAK,IAAI,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;IAC/B,MAAM,EAAE,WAAW,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,6BAA6B,EAAE,cAAc,CAAC;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,eAAe,EAAE,WAAW,CAAC;IAC9C;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAu0BlE;AAuRD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,uBAAuB,CACrC,oBAAoB,EAAE,WAAW,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAC3D,2BAA2B,CAAC,EAAE,MAAM,GACnC,MAAM,CAuCR;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,oBAAoB,EAAE,MAAM,GAAG,MAAM,EAAE,CAgB3E;AAsCD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,aAAa,EAClB,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CA0Ef;AA4QD,wBAAgB,cAAc,IAAI,WAAW,EAAE,CAkC9C;AAGD,YAAY,EAAE,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/chat/handler.ts"],"names":[],"mappings":"AAgDA,OAAO,EACL,mBAAmB,EACnB,cAAc,EAEd,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC9B,MAAM,uBAAuB,CAAC;AAI/B,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,GAC9B,CAAC;AAGF,OAAO,EACL,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,2BAA2B,EAC3B,wBAAwB,EACxB,2BAA2B,EAC3B,yBAAyB,EACzB,6BAA6B,EAC7B,2BAA2B,EAC3B,4BAA4B,EAC5B,KAAK,oBAAoB,GAC1B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,EAOL,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AA4B1B,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAOL,KAAK,WAAW,EAEjB,MAAM,eAAe,CAAC;AAWvB,OAAO,EAGL,KAAK,SAAS,EAEd,KAAK,cAAc,EAEpB,MAAM,kBAAkB,CAAC;AAY1B,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,aAAa,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;IAClB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,6BAA6B,EAAE,cAAc,CAAC;IACtE,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CAyP7D;AAED,UAAU,aAAa;IACrB,GAAG,EAAE,aAAa,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,CAAC,GAAG,EAAE,SAAS,KAAK,IAAI,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;IAC/B,MAAM,EAAE,WAAW,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,6BAA6B,EAAE,cAAc,CAAC;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,eAAe,EAAE,WAAW,CAAC;IAC9C;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA28BlE;AAmVD,wBAAgB,uBAAuB,CACrC,oBAAoB,EAAE,WAAW,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAC3D,2BAA2B,CAAC,EAAE,MAAM,GACnC,MAAM,CAuCR;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,oBAAoB,EAAE,MAAM,GAAG,MAAM,EAAE,CAgB3E;AAsCD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,aAAa,EAClB,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CA0Ef;AA4QD,wBAAgB,cAAc,IAAI,WAAW,EAAE,CAkC9C;AAGD,YAAY,EAAE,WAAW,EAAE,CAAC"}
@@ -37,9 +37,11 @@ import { adaptToolset } from "./agent-tool-adapter.js";
37
37
  import { pruneOldToolResults } from "./tool-result-truncation.js";
38
38
  import { dumpSystemPrompt } from "./dump-system-prompt.js";
39
39
  import { SqliteSessionRepo } from "./sqlite-session-repo.js";
40
- import { drainPending as drainInbox, renderForPrompt as renderInboxForPrompt, extractEvents, markDeliveredFromMessage, } from "./session-inbox.js";
40
+ import { drainPending as drainInbox, renderForPrompt as renderInboxForPrompt, extractEvents, enqueue as enqueueInbox, markDeliveredFromMessage, } from "./session-inbox.js";
41
41
  import { registerActiveHarness, registerUserSendChannel, } from "./active-harnesses.js";
42
42
  import { SqliteSessionStorage } from "./sqlite-session-storage.js";
43
+ import { createLogger, elapsedMs, summarizeToolArgs, estimateToolResultBytes, } from "../setup/logger.js";
44
+ const log = createLogger("handler");
43
45
  import { filterSkillsForTenant, loadSkillsForPlugin, } from "../core/plugins/skills.js";
44
46
  import { loadTenantSkills } from "../core/tenant-skills.js";
45
47
  import { fileURLToPath } from "node:url";
@@ -632,11 +634,33 @@ export async function runPrompt(args) {
632
634
  const ev = event;
633
635
  if (ev.type === "tool_execution_start") {
634
636
  const tc = event;
635
- outstandingToolCalls.set(tc.toolCallId, { name: tc.toolName });
637
+ const startedAt = process.hrtime.bigint();
638
+ outstandingToolCalls.set(tc.toolCallId, {
639
+ name: tc.toolName,
640
+ startedAt,
641
+ });
642
+ // Tool call debug trail (Yu, 2026-09-14 09:03): without this
643
+ // there was no way to correlate a bridge hang with which tool
644
+ // + args the agent had asked for. Only emitted at debug
645
+ // level so info-level logs stay quiet.
646
+ const rawArgs = tc.input ?? tc.arguments ?? tc.args;
647
+ log.debug(`tool_start name=${tc.toolName} id=${tc.toolCallId} args=${summarizeToolArgs(rawArgs)}`);
636
648
  }
637
649
  else if (ev.type === "tool_execution_end") {
638
650
  const te = event;
651
+ const started = outstandingToolCalls.get(te.toolCallId);
639
652
  outstandingToolCalls.delete(te.toolCallId);
653
+ if (started) {
654
+ const duration = elapsedMs(started.startedAt);
655
+ const outcome = te.error != null ? "error" : "ok";
656
+ const resultBytes = estimateToolResultBytes(te.result ?? te.output);
657
+ log.debug(`tool_end name=${started.name} id=${te.toolCallId} duration_ms=${duration} outcome=${outcome} result_bytes=${resultBytes}`);
658
+ }
659
+ else {
660
+ // Endpoint fired without a matching start — either recovery
661
+ // path or the start event was swallowed. Log so we notice.
662
+ log.debug(`tool_end id=${te.toolCallId} (no matching start; likely recovery or lost event)`);
663
+ }
640
664
  }
641
665
  bridgeHarnessEventToWs(event, {
642
666
  ctx,
@@ -807,37 +831,63 @@ export async function runPrompt(args) {
807
831
  // context to probe environment state before re-issuing the same
808
832
  // call — and to bail out and tell the user if the tool is
809
833
  // fundamentally unavailable.
810
- let recoveryAttempts = 0;
811
- const MAX_RECOVERY = 2;
812
- while (recoveryAttempts < MAX_RECOVERY) {
834
+ // Auto-recover from transient errors by scheduling a NEW turn
835
+ // via the session inbox, not harness.prompt() within this turn.
836
+ //
837
+ // Root cause (Yu, 2026-09-14): harness.prompt() reuses the same
838
+ // toolset snapshot built at the top of handleTurn. If a bridge
839
+ // tool failed because the connection dropped, the toolset still
840
+ // holds the dead BridgeConn — so the retry always fails again.
841
+ // Meanwhile, the user typing "继续" triggers a fresh handleTurn
842
+ // which calls refreshStaleToolsets() + buildToolset(), picks up
843
+ // the reconnected bridge, and succeeds.
844
+ //
845
+ // Fix: enqueue the retry prompt into the session inbox. The
846
+ // inbox flush triggers a brand-new handleTurn with fresh tool
847
+ // resolution — identical to what a user message does.
848
+ {
813
849
  const lastRow = lastAssistantRow;
814
- if (!lastRow)
815
- break;
816
- try {
817
- const parsed = JSON.parse(lastRow.content);
818
- if (parsed.stopReason !== "error" && parsed.stopReason !== "aborted")
819
- break;
820
- }
821
- catch {
822
- break;
850
+ if (lastRow && !signal.aborted) {
851
+ let needsRecovery = false;
852
+ try {
853
+ const parsed = JSON.parse(lastRow.content);
854
+ needsRecovery = parsed.stopReason === "error" || parsed.stopReason === "aborted";
855
+ }
856
+ catch { /* not JSON or no stopReason */ }
857
+ if (needsRecovery) {
858
+ // Guard against infinite retry loops: check how many recent
859
+ // auto-recovery notes already exist in this session. If we've
860
+ // already retried MAX times, stop and let the user intervene.
861
+ const MAX_AUTO_RECOVERY = 2;
862
+ const recentRecoveries = countRecentRecoveryNotes(ctx, session.id);
863
+ if (recentRecoveries >= MAX_AUTO_RECOVERY) {
864
+ console.log(`[handler] auto-recovery: already retried ${recentRecoveries} times, stopping to avoid infinite loop`);
865
+ }
866
+ else {
867
+ // Purge orphaned tool_result rows BEFORE scheduling retry.
868
+ // A failed turn can leave tool_result rows without matching
869
+ // tool_use — Anthropic rejects these with 400. Clean up now
870
+ // so the retry turn starts with valid history.
871
+ try {
872
+ const purged = purgeOrphanedToolResults(ctx, session.id);
873
+ if (purged > 0) {
874
+ console.log(`[handler] auto-recovery: pre-purged ${purged} orphaned toolResult row(s)`);
875
+ }
876
+ }
877
+ catch { /* best effort */ }
878
+ const retryPrompt = buildAutoRecoveryPrompt(outstandingToolCalls, lastRow.content);
879
+ console.log(`[handler] auto-recovery ${recentRecoveries + 1}/${MAX_AUTO_RECOVERY}: scheduling new turn via inbox in 3s`);
880
+ // 3s delay lets bridge reconnect, then a fresh handleTurn
881
+ // re-resolves toolsets with the live connection.
882
+ setTimeout(() => {
883
+ void enqueueInbox(ctx, session.id, {
884
+ kind: "system_note",
885
+ text: `[auto-recovery ${recentRecoveries + 1}/${MAX_AUTO_RECOVERY}] ${retryPrompt}`,
886
+ });
887
+ }, 3000);
888
+ }
889
+ }
823
890
  }
824
- // Don't retry if user explicitly aborted
825
- if (signal.aborted)
826
- break;
827
- recoveryAttempts++;
828
- console.log(`[handler] auto-recovery ${recoveryAttempts}/${MAX_RECOVERY} (stopReason=error/aborted, retrying)`);
829
- // Brief delay before retry to let transient issues resolve
830
- await new Promise((r) => setTimeout(r, 2000));
831
- // Pass the full assistant JSON as the second-tier data source:
832
- // pi emits tool_execution_start/end in pairs even on aborts, so
833
- // outstandingToolCalls is nearly always empty by the time we
834
- // land here. The persisted assistant content still carries the
835
- // toolCall blocks that were being invoked — that's what actually
836
- // identifies the failing tool in Yu's bridge-disconnect scenario.
837
- const retryPrompt = buildAutoRecoveryPrompt(outstandingToolCalls, lastRow?.content);
838
- console.log(`[handler] auto-recovery prompt (first 200 chars): ${retryPrompt.slice(0, 200)}`);
839
- await harness.prompt(retryPrompt);
840
- await harness.waitForIdle();
841
891
  }
842
892
  }
843
893
  catch (err) {
@@ -854,14 +904,44 @@ export async function runPrompt(args) {
854
904
  console.log(`[handler] catch: ${errMsg.slice(0, 300)}`);
855
905
  if (/tool_use_id.*tool_result|tool_result.*tool_use/i.test(errMsg)) {
856
906
  console.log(`[handler] detected orphaned tool_result error, session=${session.id}`);
907
+ // The orphan is NOT a DB row — it's generated during pi-agent-core's
908
+ // session-tree → Anthropic wire-format conversion (compaction can
909
+ // break tool_use ↔ tool_result adjacency). Row-level purge won't
910
+ // help. Force a full compaction to rebuild the history cleanly.
857
911
  try {
858
- const purged = purgeOrphanedToolResults(ctx, session.id);
859
- if (purged > 0) {
860
- console.log(`[handler] purged ${purged} orphaned toolResult row(s) from session ${session.id}`);
861
- }
912
+ console.log(`[handler] forcing compaction to clear corrupted history...`);
913
+ await compactSession({
914
+ ctx,
915
+ userId,
916
+ oldSession: session,
917
+ pi: loadAgentHistoryForSession(ctx, session.id, {
918
+ api: modelInfo.api,
919
+ provider: modelInfo.providerId,
920
+ model: modelInfo.modelId,
921
+ }).messages,
922
+ rows: loadAgentHistoryForSession(ctx, session.id, {
923
+ api: modelInfo.api,
924
+ provider: modelInfo.providerId,
925
+ model: modelInfo.modelId,
926
+ }).rows,
927
+ modelInfo,
928
+ signal: new AbortController().signal,
929
+ });
930
+ console.log(`[handler] compaction done, scheduling retry via inbox`);
931
+ setTimeout(() => {
932
+ void enqueueInbox(ctx, session.id, {
933
+ kind: "system_note",
934
+ text: "History was compacted to fix a corrupted tool_result chain. Continue where you left off.",
935
+ });
936
+ }, 1000);
862
937
  }
863
- catch (purgeErr) {
864
- console.warn(`[handler] purgeOrphanedToolResults failed: ${purgeErr}`);
938
+ catch (compactErr) {
939
+ console.warn(`[handler] forced compaction failed: ${compactErr}`);
940
+ // Fallback: try the old row-level purge
941
+ try {
942
+ purgeOrphanedToolResults(ctx, session.id);
943
+ }
944
+ catch { /* best effort */ }
865
945
  }
866
946
  }
867
947
  // pi harness state-machine errors are NOT provider failures —
@@ -986,9 +1066,11 @@ export async function runPrompt(args) {
986
1066
  // After every successful turn, decide whether to fire it.
987
1067
  // Also fires when the agent explicitly called compact_context mid-turn
988
1068
  // (which deferred to this post-turn hook via compactRef.requestedByAgent).
1069
+ let compacted = false;
989
1070
  if (!streamErrorSent || compactRef?.requestedByAgent) {
990
1071
  if (compactRef)
991
1072
  compactRef.requestedByAgent = false;
1073
+ const preCompactSession = session;
992
1074
  await maybeAutoCompact({
993
1075
  session,
994
1076
  piSession,
@@ -997,6 +1079,7 @@ export async function runPrompt(args) {
997
1079
  send,
998
1080
  compactionSettings,
999
1081
  onSuccessRefresh: () => {
1082
+ compacted = true;
1000
1083
  // Refresh after compaction: same default page size as the
1001
1084
  // initial fetch. The compacted session is what the client
1002
1085
  // wants to see; we don't try to preserve the older paged-in
@@ -1010,6 +1093,34 @@ export async function runPrompt(args) {
1010
1093
  },
1011
1094
  });
1012
1095
  }
1096
+ // After successful post-turn compaction, check if the agent was
1097
+ // mid-task. The last assistant message may have tool calls that
1098
+ // completed, but the agent intended to do more. Without a nudge
1099
+ // the session goes silent — the user has to manually type "继续".
1100
+ // Inject a "continue" via inbox so a fresh handleTurn picks up
1101
+ // with the newly-compacted (smaller) context.
1102
+ if (compacted && !signal.aborted) {
1103
+ const lastRow = lastAssistantRow;
1104
+ if (lastRow)
1105
+ try {
1106
+ const parsed = JSON.parse(lastRow.content);
1107
+ // Heuristic: the agent was mid-task if its last turn contained
1108
+ // tool calls (it was doing work, not just chatting).
1109
+ const hadToolCalls = Array.isArray(parsed.content) &&
1110
+ parsed.content.some((b) => b.type === "tool_use" || b.type === "toolCall");
1111
+ if (hadToolCalls) {
1112
+ console.log(`[handler] post-compaction: agent had tool calls in last turn, injecting continue via inbox`);
1113
+ setTimeout(() => {
1114
+ void enqueueInbox(ctx, session.id, {
1115
+ kind: "system_note",
1116
+ text: "Context was automatically compacted to free space. " +
1117
+ "Continue where you left off — the task is still in progress.",
1118
+ });
1119
+ }, 1000);
1120
+ }
1121
+ }
1122
+ catch { /* not JSON, skip */ }
1123
+ }
1013
1124
  // Emit stream_end so the UI re-enables the send button.
1014
1125
  //
1015
1126
  // Skip the persisted row when its rendered text is empty —
@@ -1288,6 +1399,31 @@ function bridgeHarnessEventToWs(event, args) {
1288
1399
  * Exported so it can be unit-tested; not intended for external
1289
1400
  * callers.
1290
1401
  */
1402
+ /**
1403
+ * Count recent auto-recovery inbox notes for this session (last 60s).
1404
+ * Used to cap the retry loop and prevent infinite recovery cycles.
1405
+ */
1406
+ function countRecentRecoveryNotes(ctx, sessionId) {
1407
+ const cutoff = Date.now() - 60_000;
1408
+ try {
1409
+ const rows = ctx.db
1410
+ .prepare(`SELECT payload FROM session_inbox
1411
+ WHERE target_session_id = ? AND created_at > ?`)
1412
+ .all(sessionId, cutoff);
1413
+ return rows.filter((r) => {
1414
+ try {
1415
+ const m = JSON.parse(r.payload);
1416
+ return typeof m.text === "string" && m.text.includes("[auto-recovery");
1417
+ }
1418
+ catch {
1419
+ return false;
1420
+ }
1421
+ }).length;
1422
+ }
1423
+ catch {
1424
+ return 0;
1425
+ }
1426
+ }
1291
1427
  export function buildAutoRecoveryPrompt(outstandingToolCalls, lastAssistantMessageContent) {
1292
1428
  // Prefer the outstanding map when it has anything at all.
1293
1429
  const outstanding = [...outstandingToolCalls.values()];
@@ -1753,9 +1889,11 @@ function filterOrphanedToolResults(entries) {
1753
1889
  continue;
1754
1890
  for (const part of content) {
1755
1891
  const p = part;
1756
- if (p.type === "toolCall") {
1892
+ if (p.type === "toolCall" || p.type === "tool_use") {
1757
1893
  if (p.id)
1758
1894
  toolUseIds.add(p.id);
1895
+ if (p.toolCallId)
1896
+ toolUseIds.add(p.toolCallId);
1759
1897
  }
1760
1898
  }
1761
1899
  }
@@ -1804,8 +1942,10 @@ function purgeOrphanedToolResults(ctx, sessionId) {
1804
1942
  const content = parsed.content ?? parsed;
1805
1943
  if (Array.isArray(content)) {
1806
1944
  for (const part of content) {
1807
- if (part.type === "toolCall" && part.id)
1808
- toolUseIds.add(part.id);
1945
+ // pi stores as "toolCall", Anthropic wire format as "tool_use"
1946
+ if ((part.type === "toolCall" || part.type === "tool_use") && (part.id || part.toolCallId)) {
1947
+ toolUseIds.add(part.id ?? part.toolCallId);
1948
+ }
1809
1949
  }
1810
1950
  }
1811
1951
  }
@@ -1818,7 +1958,8 @@ function purgeOrphanedToolResults(ctx, sessionId) {
1818
1958
  continue;
1819
1959
  try {
1820
1960
  const parsed = JSON.parse(row.content);
1821
- const tcId = parsed.toolCallId;
1961
+ // pi stores as "toolCallId", Anthropic wire as "tool_use_id"
1962
+ const tcId = parsed.toolCallId ?? parsed.tool_use_id;
1822
1963
  if (tcId && !toolUseIds.has(tcId)) {
1823
1964
  orphanRowIds.push(row.id);
1824
1965
  }