@wrongstack/core 0.313.1 → 0.316.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chronicle/index.js +31 -31
- package/dist/chronicle/legacy-journal-import.d.ts +0 -6
- package/dist/chronicle/metrics-ingest.d.ts +14 -0
- package/dist/chronicle/metrics-schema.d.ts +10 -1
- package/dist/chronicle/metrics-store.d.ts +20 -2
- package/dist/chronicle/project-server.js +39 -31
- package/dist/chronicle/sink.d.ts +13 -0
- package/dist/chronicle/sqlite-journal.d.ts +20 -2
- package/dist/{chunk-WOEZV3Q4.js → chunk-25LSLC3G.js} +2 -1
- package/dist/{chunk-AI4LLQ2U.js → chunk-27QIURLN.js} +229 -12
- package/dist/{chunk-I45KAW2N.js → chunk-3AGWVSS7.js} +38 -450
- package/dist/chunk-3BDV7RL3.js +211 -0
- package/dist/{chunk-NFAYHPRH.js → chunk-3ECENVHL.js} +10 -8
- package/dist/{chunk-FW5LDDMJ.js → chunk-45WI72ZB.js} +1576 -1156
- package/dist/{chunk-AT4V4XGI.js → chunk-4BMVPOHO.js} +539 -313
- package/dist/{chunk-YBUJCZ57.js → chunk-4I7HGVVA.js} +17 -5
- package/dist/{chunk-JVGPR3TS.js → chunk-4LCZVJVF.js} +7 -55
- package/dist/{chunk-4ZHTKJHM.js → chunk-4SJNS7OB.js} +36 -2
- package/dist/{chunk-AD7ZFZCS.js → chunk-5BMIE7QG.js} +3 -3
- package/dist/{chunk-IQ35HMQL.js → chunk-5YPSXQXM.js} +71 -24
- package/dist/chunk-6FHWXLPT.js +10 -0
- package/dist/{chunk-FIV4IJL7.js → chunk-6H5GEOET.js} +163 -20
- package/dist/{chunk-VB3H5WWB.js → chunk-6NWEVFEL.js} +2 -2
- package/dist/{chunk-E6I5RXVK.js → chunk-AYW5YINX.js} +2 -2
- package/dist/{chunk-DEIQMBD2.js → chunk-B7F4WVP4.js} +2 -2
- package/dist/{chunk-XQ2XGIBF.js → chunk-BPBKKBUK.js} +6 -7
- package/dist/{chunk-K4YUNKU7.js → chunk-BYD345OX.js} +2 -2
- package/dist/{chunk-LZDDLNCZ.js → chunk-C27KDLOX.js} +4 -4
- package/dist/{chunk-6AKOTGLM.js → chunk-CU546ESW.js} +7 -7
- package/dist/{chunk-ZZ4V3WIZ.js → chunk-DLHHXUAM.js} +10 -1
- package/dist/{chunk-BAKVJLEV.js → chunk-DOTHQSJ2.js} +28 -16
- package/dist/{chunk-KBZGE6LN.js → chunk-DYLPMV4Q.js} +4 -3
- package/dist/{chunk-52RTVH4M.js → chunk-F3JWNST7.js} +2 -2
- package/dist/{chunk-WEBMSQP2.js → chunk-FC2ZGRF3.js} +21 -4
- package/dist/{chunk-FHAMQS7Q.js → chunk-FCBTO76Z.js} +76 -146
- package/dist/{chunk-4LJJYBAU.js → chunk-FHZVFYSL.js} +9 -1
- package/dist/{chunk-W4A6QA3D.js → chunk-FWNLEZYG.js} +4 -3
- package/dist/{chunk-OJZEX73F.js → chunk-GDQ7CZRQ.js} +5 -1
- package/dist/{chunk-QVOVMXJR.js → chunk-GJJGJB2R.js} +32 -9
- package/dist/{chunk-4XV6IR37.js → chunk-HI3JGZR2.js} +7 -6
- package/dist/{chunk-FSWCWSTS.js → chunk-HMLIGAU7.js} +26 -26
- package/dist/{chunk-PFXFQQRT.js → chunk-HXKZ7OBQ.js} +7 -43
- package/dist/{chunk-2FQL4PDC.js → chunk-I7DALG7J.js} +5 -5
- package/dist/{chunk-GM54GJBU.js → chunk-IRUTEHCE.js} +3 -3
- package/dist/{chunk-TYW2GI5I.js → chunk-J2WRWR37.js} +40 -13
- package/dist/{chunk-ZEHUCJHY.js → chunk-JFPJ6LLQ.js} +2 -2
- package/dist/chunk-JMULFFLN.js +9 -0
- package/dist/{chunk-RD6DEE36.js → chunk-JO4MGB6Z.js} +94 -8
- package/dist/{chunk-5DSMEVA4.js → chunk-K6BXGITA.js} +7 -7
- package/dist/{chunk-O74MJHXU.js → chunk-KZMSZV32.js} +2 -2
- package/dist/{chunk-GFJOMSHG.js → chunk-LA7CLDRW.js} +6 -2
- package/dist/{chunk-M2WF6AXG.js → chunk-LSMMOS2Z.js} +7 -3
- package/dist/{chunk-ASX6AW36.js → chunk-LXWCMQNJ.js} +3 -3
- package/dist/{chunk-WZO23AWO.js → chunk-M27LIEKV.js} +72 -22
- package/dist/{chunk-JTHKOYWU.js → chunk-MJPHJRAL.js} +15 -15
- package/dist/{chunk-G5AAKEEZ.js → chunk-MTL4O5YR.js} +5 -5
- package/dist/chunk-NHJ75HXY.js +1 -0
- package/dist/{chunk-ZYU2YOJ6.js → chunk-NIOAGFPB.js} +29 -15
- package/dist/{chunk-Y2GE5TSI.js → chunk-NS52ISWS.js} +2 -2
- package/dist/{chunk-UIWONWVG.js → chunk-NSOFPKGT.js} +541 -148
- package/dist/{chunk-RCQYT2I4.js → chunk-NVIZG63R.js} +2 -2
- package/dist/{chunk-EPC3GZD4.js → chunk-OOYZPOXM.js} +2 -2
- package/dist/{chunk-7ZHZE5EO.js → chunk-OSGLYW5J.js} +2 -2
- package/dist/{chunk-CE4ZOCOI.js → chunk-PFCMSSUF.js} +9 -9
- package/dist/{chunk-GFXZTHNI.js → chunk-PGRVXIMY.js} +5 -5
- package/dist/{chunk-JJPMWPB6.js → chunk-PISBFBJN.js} +6 -6
- package/dist/chunk-POOOIWE5.js +9 -0
- package/dist/{chunk-5RP3QTRL.js → chunk-PQNS3U7Z.js} +20 -3
- package/dist/{chunk-3E244UDA.js → chunk-QSTSJOCK.js} +52 -6
- package/dist/{chunk-YUPMXOGP.js → chunk-RFQMOKWK.js} +3 -3
- package/dist/chunk-RTOTNK3P.js +484 -0
- package/dist/{chunk-SN2DQRL6.js → chunk-RXKSDUAU.js} +7 -7
- package/dist/{chunk-T3KGZBU5.js → chunk-S45JPKQN.js} +189 -43
- package/dist/{chunk-YNCVSFGF.js → chunk-SAFHACFA.js} +4 -1
- package/dist/{chunk-KI4L2DUL.js → chunk-TCCHINEF.js} +114 -19
- package/dist/{chunk-AWNQ6ED6.js → chunk-THEGTRAG.js} +7 -2
- package/dist/{chunk-2KKODIQU.js → chunk-TR4LQE7L.js} +15 -15
- package/dist/{chunk-GPQ3NJPR.js → chunk-TWJMGXCI.js} +3 -3
- package/dist/{chunk-H72H4XDU.js → chunk-U45AXT5D.js} +9 -9
- package/dist/{chunk-62V2AFVG.js → chunk-U4FWBSO3.js} +160 -73
- package/dist/{chunk-A455ATZB.js → chunk-UBGDX6FI.js} +8 -1
- package/dist/chunk-UJILKYES.js +75 -0
- package/dist/{chunk-KSJ5UTP7.js → chunk-ULMAQV3T.js} +84 -4
- package/dist/{chunk-CB2HFKTP.js → chunk-VVBP6KGT.js} +1 -1
- package/dist/{chunk-RPOA7IAE.js → chunk-W5VITDM2.js} +43 -25
- package/dist/{chunk-QRTG4XTI.js → chunk-WAHQVNWT.js} +7 -1
- package/dist/{chunk-SQP64RCS.js → chunk-WGGYRBXH.js} +3019 -2976
- package/dist/{chunk-K3G6JBUC.js → chunk-WLYKU45P.js} +679 -141
- package/dist/chunk-WP7BVANJ.js +53 -0
- package/dist/{chunk-GAZEWQEZ.js → chunk-XF4VUMKW.js} +2 -2
- package/dist/{chunk-PAAWLLZX.js → chunk-Y5YXDM7D.js} +2 -2
- package/dist/{chunk-65RDIOR2.js → chunk-YOPAUVU4.js} +162 -44
- package/dist/{chunk-FFSX2QDQ.js → chunk-YPT4A46F.js} +3 -3
- package/dist/chunk-YWXT3JNM.js +425 -0
- package/dist/{chunk-EFEHDQGY.js → chunk-ZHZ2Y2DH.js} +64 -10
- package/dist/{chunk-CQQL4ZOQ.js → chunk-ZT7JNIGX.js} +4 -3
- package/dist/coordination/agent-monitor.d.ts +9 -1
- package/dist/coordination/agent-subagent-runner.d.ts +1 -1
- package/dist/coordination/agents/agent-prompts.d.ts +8 -0
- package/dist/coordination/agents/index.d.ts +33 -1
- package/dist/coordination/agents/index.js +52 -22
- package/dist/coordination/agents/phase3-wave1-platform.d.ts +1 -3
- package/dist/coordination/agents/phase3-wave2-meta.d.ts +1 -3
- package/dist/coordination/agents/phase8-wave3-products.d.ts +1 -3
- package/dist/coordination/agents/phase9-wave4-platform-meta.d.ts +1 -3
- package/dist/coordination/agents/project-agent-identity.d.ts +11 -11
- package/dist/coordination/agents/types.d.ts +13 -2
- package/dist/coordination/brain-monitor.d.ts +30 -7
- package/dist/coordination/collab-debug.d.ts +1 -1
- package/dist/coordination/collab-pause.d.ts +1 -1
- package/dist/coordination/delegate-tool.d.ts +7 -5
- package/dist/coordination/director-kanban-queue-helpers.d.ts +1 -0
- package/dist/coordination/director-options.d.ts +7 -0
- package/dist/coordination/director-spawn-model.d.ts +17 -0
- package/dist/coordination/director-tools.d.ts +2 -2
- package/dist/coordination/director.d.ts +12 -0
- package/dist/coordination/dispatcher.d.ts +8 -0
- package/dist/coordination/fleet-bus.d.ts +35 -4
- package/dist/coordination/fleet-supervisor.d.ts +10 -2
- package/dist/coordination/fleet.d.ts +16 -7
- package/dist/coordination/index.d.ts +4 -0
- package/dist/coordination/index.js +86 -49
- package/dist/coordination/kanban-dispatch-port.d.ts +2 -2
- package/dist/coordination/mailbox-http-router.d.ts +4 -3
- package/dist/coordination/mailbox-http-validation.d.ts +1 -1
- package/dist/coordination/mailbox-project-server.js +15 -15
- package/dist/coordination/mailbox-types.d.ts +24 -6
- package/dist/coordination/model-tier-leader.d.ts +122 -0
- package/dist/coordination/model-tier.d.ts +122 -0
- package/dist/coordination/multi-agent-coordinator.d.ts +24 -0
- package/dist/coordination/origin-session.d.ts +16 -0
- package/dist/coordination/provider-status-tracker.d.ts +37 -5
- package/dist/coordination/session-note-hub.d.ts +44 -0
- package/dist/coordination/session-note-tool.d.ts +10 -0
- package/dist/core/agent.d.ts +1 -1
- package/dist/core/context.d.ts +22 -3
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +130 -38
- package/dist/core/request-conversation-binding.d.ts +37 -0
- package/dist/core/session-notes.d.ts +25 -0
- package/dist/core/system-prompt-builder.d.ts +11 -2
- package/dist/core/system-prompt-variants.d.ts +78 -0
- package/dist/design/index.js +3 -3
- package/dist/execution/auto-compaction-middleware.d.ts +11 -0
- package/dist/execution/autonomy-brain.d.ts +2 -1
- package/dist/execution/autonomy-prompt-contributor.d.ts +7 -2
- package/dist/execution/compaction-core.d.ts +4 -4
- package/dist/execution/eternal-autonomy.d.ts +1 -1
- package/dist/execution/index.js +138 -129
- package/dist/execution/model-runtime.d.ts +0 -7
- package/dist/execution/parallel-eternal-engine.d.ts +7 -7
- package/dist/extension/index.js +3 -3
- package/dist/goal/index.js +6 -6
- package/dist/hooks/index.js +3 -3
- package/dist/hq/index.js +14 -12
- package/dist/hq/persistence/event-log.d.ts +0 -1
- package/dist/hq/persistence.d.ts +4 -4
- package/dist/hq/publisher.d.ts +2 -0
- package/dist/index.d.ts +14 -11
- package/dist/index.js +728 -635
- package/dist/infrastructure/index.d.ts +4 -4
- package/dist/infrastructure/index.js +281 -260
- package/dist/infrastructure/token-counter.d.ts +9 -0
- package/dist/kernel/events/agent-events.d.ts +77 -3
- package/dist/kernel/events/brain-events.d.ts +12 -0
- package/dist/kernel/events/provider-events.d.ts +7 -1
- package/dist/kernel/events/session-events.d.ts +14 -0
- package/dist/kernel/events/tool-events.d.ts +13 -0
- package/dist/kernel/index.js +4 -4
- package/dist/mailbox-attach.d.ts +1 -1
- package/dist/models/index.js +10 -9
- package/dist/observability/event-bridge.d.ts +25 -5
- package/dist/observability/index.d.ts +9 -8
- package/dist/observability/index.js +459 -351
- package/dist/observability/tool-usage-source.d.ts +65 -0
- package/dist/plugin/index.d.ts +5 -0
- package/dist/plugin/index.js +66 -50
- package/dist/plugins/auto-review-plugin.d.ts +3 -2
- package/dist/plugins/review-finding-commands.d.ts +0 -8
- package/dist/{review-finding-store-DKAV3AS6.js → plugins/review-finding-store.js} +7 -6
- package/dist/plugins/review-finding-types.js +19 -0
- package/dist/plugins/review-report-integration.js +21 -0
- package/dist/{review-report-store-OWY3EC3N.js → plugins/review-report-store.js} +7 -6
- package/dist/plugins/review-report-types.js +13 -0
- package/dist/prompts/index.js +5 -5
- package/dist/registry/index.js +3 -3
- package/dist/registry/tool-registry.d.ts +59 -9
- package/dist/{registry-R23UBE7H.js → registry-7N4O3E7W.js} +5 -5
- package/dist/security/capabilities.d.ts +6 -0
- package/dist/security/file-permissions.d.ts +1 -1
- package/dist/security/index.js +47 -33
- package/dist/security/permission-policy.d.ts +12 -1
- package/dist/security/secret-scrubber.d.ts +15 -0
- package/dist/session-catalog/index.d.ts +1 -0
- package/dist/session-catalog/index.js +19 -14
- package/dist/session-catalog/project-server.js +30 -13
- package/dist/session-catalog/protocol.d.ts +32 -1
- package/dist/session-catalog/registry.d.ts +17 -1
- package/dist/session-catalog/session-agents.d.ts +56 -0
- package/dist/session-catalog/store-rebuild.d.ts +14 -0
- package/dist/session-catalog/store-schema.d.ts +26 -0
- package/dist/session-catalog/store.d.ts +27 -1
- package/dist/session-note-attach.d.ts +9 -0
- package/dist/skills/index.js +5 -5
- package/dist/statusline/index.d.ts +43 -0
- package/dist/statusline/index.js +102 -0
- package/dist/storage/board-store-port.d.ts +2 -2
- package/dist/storage/config-loader/in-project-policy.d.ts +29 -0
- package/dist/storage/config-loader.d.ts +3 -2
- package/dist/storage/file-session-writer.d.ts +4 -2
- package/dist/storage/goal-coordination.d.ts +6 -1
- package/dist/storage/goal-kanban.d.ts +4 -1
- package/dist/storage/index.d.ts +12 -4
- package/dist/storage/index.js +76 -61
- package/dist/storage/input-history-store.d.ts +2 -4
- package/dist/storage/memory-curator.d.ts +133 -0
- package/dist/storage/session-agent-attribution.d.ts +39 -0
- package/dist/storage/session-doctor.d.ts +109 -0
- package/dist/storage/session-event-bridge.d.ts +5 -7
- package/dist/storage/session-outcome.d.ts +42 -0
- package/dist/storage/session-recovery.d.ts +55 -0
- package/dist/storage/session-store/load-session-data.d.ts +11 -0
- package/dist/storage/session-store/prune-helpers.d.ts +1 -1
- package/dist/storage/session-store/resume-session.d.ts +0 -2
- package/dist/storage/session-store/types.d.ts +8 -0
- package/dist/storage/session-store.d.ts +3 -3
- package/dist/storage/session-summary-tracker.d.ts +8 -1
- package/dist/storage/session-write-buffer.d.ts +18 -0
- package/dist/storage/session-writer-truncate.d.ts +2 -0
- package/dist/tasking/index.js +2 -2
- package/dist/tools/fallback-manage-tools.d.ts +6 -4
- package/dist/tools/index.js +21 -21
- package/dist/tools/model-tier-set-tool.d.ts +66 -0
- package/dist/types/agent-bridge.d.ts +2 -1
- package/dist/types/config/mcp-features.d.ts +7 -0
- package/dist/types/config/model-tiers.d.ts +148 -0
- package/dist/types/config/root.d.ts +9 -0
- package/dist/types/config/tools.d.ts +73 -0
- package/dist/types/config.d.ts +1 -0
- package/dist/types/context.d.ts +2 -0
- package/dist/types/default-config.d.ts +6 -0
- package/dist/types/errors.d.ts +2 -1
- package/dist/types/index.d.ts +11 -9
- package/dist/types/index.js +25 -15
- package/dist/types/multi-agent.d.ts +43 -0
- package/dist/types/plugin.d.ts +1 -1
- package/dist/types/provider.d.ts +15 -0
- package/dist/types/runtime-capability-manifest.d.ts +1 -1
- package/dist/types/secret-scrubber.d.ts +15 -0
- package/dist/types/session-markers.d.ts +37 -10
- package/dist/types/session-markers.js +1 -1
- package/dist/types/session-timeline.d.ts +197 -0
- package/dist/types/session-timeline.js +12 -0
- package/dist/types/session.d.ts +172 -11
- package/dist/types/system-prompt.d.ts +27 -0
- package/dist/utils/config-backup.d.ts +9 -1
- package/dist/utils/heap-watchdog.js +2 -2
- package/dist/utils/index.js +50 -48
- package/dist/utils/message-invariants.d.ts +4 -1
- package/dist/utils/regex-guard.d.ts +1 -1
- package/dist/utils/socket-path.d.ts +1 -1
- package/dist/utils/token-estimate.d.ts +6 -2
- package/dist/utils/wstack-paths.d.ts +2 -0
- package/dist/wiring/proxy-rewrite.d.ts +28 -0
- package/dist/wiring/proxy-rewrite.js +7 -1
- package/dist/worktree/index.js +2 -2
- package/instructions/agents/explore-companion.md +45 -18
- package/instructions/agents/memory-curator.md +34 -22
- package/instructions/coordination/director-preamble.md +12 -7
- package/instructions/coordination/subagent-baseline.md +26 -5
- package/instructions/leader-after-task.md +4 -1
- package/instructions/llm/chimera-review.md +30 -9
- package/instructions/llm/memory-curator.md +31 -0
- package/instructions/sections/tool/delegation-compact.md +2 -0
- package/instructions/sections/tool/delegation-full.md +5 -1
- package/instructions/sections/tool/mailbox-compact.md +5 -1
- package/instructions/sections/tool/mailbox-full.md +3 -0
- package/instructions/sections/tool/session-note-compact.md +3 -0
- package/instructions/sections/tool/session-note-full.md +15 -0
- package/instructions/system-lite.md +9 -4
- package/instructions/system-pro.md +66 -143
- package/instructions/system.md +73 -175
- package/package.json +33 -5
- package/dist/chunk-2BUUIMQB.js +0 -225
- package/dist/chunk-FAYRX2AD.js +0 -13
- package/dist/chunk-H5E77KUJ.js +0 -15
- package/dist/chunk-M7BS2K6U.js +0 -1
- package/dist/coordination/agents.d.ts +0 -8
- package/dist/coordination/transport.d.ts +0 -2
- package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +0 -28
|
@@ -4,26 +4,29 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
SessionCatalogProjectClient,
|
|
6
6
|
resolveSessionCatalogProjectServerUrl
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-F3JWNST7.js";
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import {
|
|
12
|
-
DefaultSecretScrubber
|
|
13
|
-
} from "./chunk-3E244UDA.js";
|
|
9
|
+
shouldSkipSessionDirectoryEntry
|
|
10
|
+
} from "./chunk-JMULFFLN.js";
|
|
14
11
|
import {
|
|
15
12
|
isSessionTranscriptFileName,
|
|
16
13
|
sessionScopedPath
|
|
17
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-5BMIE7QG.js";
|
|
15
|
+
import {
|
|
16
|
+
DefaultSecretScrubber
|
|
17
|
+
} from "./chunk-QSTSJOCK.js";
|
|
18
|
+
import {
|
|
19
|
+
SECRET_FILE_MODE
|
|
20
|
+
} from "./chunk-6FHWXLPT.js";
|
|
18
21
|
import {
|
|
19
22
|
buildChildEnv
|
|
20
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-6NWEVFEL.js";
|
|
21
24
|
import {
|
|
22
25
|
repairToolUseAdjacency
|
|
23
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-LSMMOS2Z.js";
|
|
24
27
|
import {
|
|
25
28
|
ulid
|
|
26
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-HI3JGZR2.js";
|
|
27
30
|
import {
|
|
28
31
|
isPidAlive
|
|
29
32
|
} from "./chunk-TRT2HRWZ.js";
|
|
@@ -31,7 +34,11 @@ import {
|
|
|
31
34
|
atomicWrite,
|
|
32
35
|
ensureDir,
|
|
33
36
|
withFileLock
|
|
34
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-OSGLYW5J.js";
|
|
38
|
+
import {
|
|
39
|
+
effectiveInputTokens,
|
|
40
|
+
totalUsageTokens
|
|
41
|
+
} from "./chunk-3ECENVHL.js";
|
|
35
42
|
import {
|
|
36
43
|
toErrorMessage
|
|
37
44
|
} from "./chunk-TBRL2RCC.js";
|
|
@@ -292,8 +299,8 @@ function sha256(content) {
|
|
|
292
299
|
return createHash("sha256").update(content).digest("hex");
|
|
293
300
|
}
|
|
294
301
|
function isInside(root, target) {
|
|
295
|
-
const
|
|
296
|
-
return
|
|
302
|
+
const relative4 = path.relative(root, target);
|
|
303
|
+
return relative4 === "" || !relative4.startsWith("..") && !path.isAbsolute(relative4);
|
|
297
304
|
}
|
|
298
305
|
function normalizeRelative(input) {
|
|
299
306
|
if (!input || path.isAbsolute(input)) return null;
|
|
@@ -305,8 +312,8 @@ function normalizeRelative(input) {
|
|
|
305
312
|
function parseNulPaths(output) {
|
|
306
313
|
return output.split("\0").map(normalizeRelative).filter((value) => value !== null);
|
|
307
314
|
}
|
|
308
|
-
function isWrongStackWorktreePath(
|
|
309
|
-
return
|
|
315
|
+
function isWrongStackWorktreePath(relative4) {
|
|
316
|
+
return relative4 === ".wrongstack/worktrees" || relative4.startsWith(".wrongstack/worktrees/");
|
|
310
317
|
}
|
|
311
318
|
var SessionCheckpointCas = class {
|
|
312
319
|
rootDir;
|
|
@@ -332,16 +339,16 @@ var SessionCheckpointCas = class {
|
|
|
332
339
|
}
|
|
333
340
|
const relativePaths = [
|
|
334
341
|
.../* @__PURE__ */ new Set([...parseNulPaths(tracked.stdout), ...parseNulPaths(untracked.stdout)])
|
|
335
|
-
].filter((
|
|
342
|
+
].filter((relative4) => !isWrongStackWorktreePath(relative4)).sort();
|
|
336
343
|
const unresolved = [];
|
|
337
344
|
let scheduledBytes = 0;
|
|
338
345
|
const captured = await mapWithConcurrency(
|
|
339
346
|
relativePaths,
|
|
340
347
|
CAPTURE_CONCURRENCY,
|
|
341
|
-
async (
|
|
342
|
-
const absolute = path.resolve(this.projectRoot, ...
|
|
348
|
+
async (relative4) => {
|
|
349
|
+
const absolute = path.resolve(this.projectRoot, ...relative4.split("/"));
|
|
343
350
|
if (!isInside(this.projectRoot, absolute)) {
|
|
344
|
-
unresolved.push({ path:
|
|
351
|
+
unresolved.push({ path: relative4, reason: "path escapes project root" });
|
|
345
352
|
return null;
|
|
346
353
|
}
|
|
347
354
|
try {
|
|
@@ -351,27 +358,27 @@ var SessionCheckpointCas = class {
|
|
|
351
358
|
const resolvedLink = path.resolve(path.dirname(absolute), linkTarget);
|
|
352
359
|
if (path.isAbsolute(linkTarget) || !isInside(this.projectRoot, resolvedLink)) {
|
|
353
360
|
unresolved.push({
|
|
354
|
-
path:
|
|
361
|
+
path: relative4,
|
|
355
362
|
reason: "symlink target escapes project root"
|
|
356
363
|
});
|
|
357
364
|
return null;
|
|
358
365
|
}
|
|
359
|
-
return { path:
|
|
366
|
+
return { path: relative4, state: "symlink", linkTarget };
|
|
360
367
|
}
|
|
361
368
|
if (!stat11.isFile()) {
|
|
362
|
-
unresolved.push({ path:
|
|
369
|
+
unresolved.push({ path: relative4, reason: "changed path is not a regular file" });
|
|
363
370
|
return null;
|
|
364
371
|
}
|
|
365
372
|
if (stat11.size > MAX_BLOB_BYTES) {
|
|
366
373
|
unresolved.push({
|
|
367
|
-
path:
|
|
374
|
+
path: relative4,
|
|
368
375
|
reason: `file exceeds ${MAX_BLOB_BYTES}-byte checkpoint blob limit`
|
|
369
376
|
});
|
|
370
377
|
return null;
|
|
371
378
|
}
|
|
372
379
|
if (scheduledBytes + stat11.size > MAX_CHECKPOINT_BYTES) {
|
|
373
380
|
unresolved.push({
|
|
374
|
-
path:
|
|
381
|
+
path: relative4,
|
|
375
382
|
reason: `checkpoint exceeds ${MAX_CHECKPOINT_BYTES}-byte aggregate blob limit`
|
|
376
383
|
});
|
|
377
384
|
return null;
|
|
@@ -380,12 +387,12 @@ var SessionCheckpointCas = class {
|
|
|
380
387
|
const content = await fsp2.readFile(absolute);
|
|
381
388
|
const blobHash = sha256(content);
|
|
382
389
|
await this.putBlob(blobHash, content);
|
|
383
|
-
return { path:
|
|
390
|
+
return { path: relative4, state: "file", blobHash, mode: stat11.mode & 511 };
|
|
384
391
|
} catch (err) {
|
|
385
392
|
if (err.code === "ENOENT") {
|
|
386
|
-
return { path:
|
|
393
|
+
return { path: relative4, state: "absent" };
|
|
387
394
|
}
|
|
388
|
-
unresolved.push({ path:
|
|
395
|
+
unresolved.push({ path: relative4, reason: toErrorMessage(err) });
|
|
389
396
|
return null;
|
|
390
397
|
}
|
|
391
398
|
}
|
|
@@ -412,11 +419,9 @@ var SessionCheckpointCas = class {
|
|
|
412
419
|
}
|
|
413
420
|
async materialize(checkpoint, targetRoot) {
|
|
414
421
|
const target = path.resolve(targetRoot);
|
|
415
|
-
if (target === this.projectRoot) {
|
|
416
|
-
throw new Error("Refusing to materialize a workspace checkpoint over the parent project root");
|
|
417
|
-
}
|
|
418
422
|
const targetStat = await fsp2.stat(target);
|
|
419
|
-
if (!targetStat.isDirectory())
|
|
423
|
+
if (!targetStat.isDirectory())
|
|
424
|
+
throw new Error(`Checkpoint target is not a directory: ${target}`);
|
|
420
425
|
const manifest = await this.loadManifest(checkpoint.manifestHash);
|
|
421
426
|
if (manifest.baseHead !== checkpoint.baseHead.toLowerCase() || manifest.coverage !== checkpoint.coverage || manifest.entries.length !== checkpoint.entryCount || manifest.unresolved.length !== checkpoint.unresolvedCount) {
|
|
422
427
|
throw new Error("Workspace checkpoint reference does not match its CAS manifest");
|
|
@@ -432,15 +437,18 @@ var SessionCheckpointCas = class {
|
|
|
432
437
|
`Checkpoint target HEAD must equal ${manifest.baseHead}; got ${targetHead.stdout.trim() || "unknown"}`
|
|
433
438
|
);
|
|
434
439
|
}
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
440
|
+
const materializingParentRoot = target === this.projectRoot;
|
|
441
|
+
if (!materializingParentRoot) {
|
|
442
|
+
const targetStatus = await this.runGit(
|
|
443
|
+
["status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignored=matching"],
|
|
444
|
+
target
|
|
445
|
+
);
|
|
446
|
+
if (targetStatus.code !== 0 || targetStatus.stdoutTruncated) {
|
|
447
|
+
throw new Error("Unable to verify that the checkpoint target is clean");
|
|
448
|
+
}
|
|
449
|
+
if (targetStatus.stdout.length > 0) {
|
|
450
|
+
throw new Error("Checkpoint target must be a clean checkout before materialization");
|
|
451
|
+
}
|
|
444
452
|
}
|
|
445
453
|
const realTarget = await fsp2.realpath(target);
|
|
446
454
|
const writtenFiles = [];
|
|
@@ -455,7 +463,8 @@ var SessionCheckpointCas = class {
|
|
|
455
463
|
throw new Error("absolute symlink target refused");
|
|
456
464
|
}
|
|
457
465
|
const resolvedLink = path.resolve(path.dirname(output), entry.linkTarget);
|
|
458
|
-
if (!isInside(target, resolvedLink))
|
|
466
|
+
if (!isInside(target, resolvedLink))
|
|
467
|
+
throw new Error("symlink target escapes checkpoint root");
|
|
459
468
|
}
|
|
460
469
|
prepared.push({
|
|
461
470
|
entry,
|
|
@@ -542,7 +551,8 @@ var SessionCheckpointCas = class {
|
|
|
542
551
|
const target = this.manifestPath(hash);
|
|
543
552
|
const existing = await fsp2.readFile(target).catch(() => null);
|
|
544
553
|
if (existing) {
|
|
545
|
-
if (sha256(existing) !== hash)
|
|
554
|
+
if (sha256(existing) !== hash)
|
|
555
|
+
throw new Error(`Corrupt checkpoint manifest collision: ${hash}`);
|
|
546
556
|
return;
|
|
547
557
|
}
|
|
548
558
|
await atomicWrite(target, encoded, { mode: 384 });
|
|
@@ -554,7 +564,8 @@ var SessionCheckpointCas = class {
|
|
|
554
564
|
}
|
|
555
565
|
async loadManifest(hash) {
|
|
556
566
|
const raw = await fsp2.readFile(this.manifestPath(hash));
|
|
557
|
-
if (sha256(raw) !== hash)
|
|
567
|
+
if (sha256(raw) !== hash)
|
|
568
|
+
throw new Error(`Checkpoint manifest integrity check failed: ${hash}`);
|
|
558
569
|
const parsed = JSON.parse(raw.toString("utf8"));
|
|
559
570
|
if (parsed.version !== 1 || typeof parsed.baseHead !== "string" || parsed.coverage !== "git-head-plus-dirty" || !Array.isArray(parsed.entries) || !Array.isArray(parsed.unresolved)) {
|
|
560
571
|
throw new Error(`Invalid workspace checkpoint manifest: ${hash}`);
|
|
@@ -575,8 +586,8 @@ var SessionCheckpointCas = class {
|
|
|
575
586
|
}
|
|
576
587
|
return parsed;
|
|
577
588
|
}
|
|
578
|
-
async safeOutputPath(target, realTarget,
|
|
579
|
-
const normalized = normalizeRelative(
|
|
589
|
+
async safeOutputPath(target, realTarget, relative4) {
|
|
590
|
+
const normalized = normalizeRelative(relative4);
|
|
580
591
|
if (!normalized) throw new Error("invalid relative path");
|
|
581
592
|
const output = path.resolve(target, ...normalized.split("/"));
|
|
582
593
|
if (!isInside(target, output)) throw new Error("path escapes checkpoint target");
|
|
@@ -584,7 +595,8 @@ var SessionCheckpointCas = class {
|
|
|
584
595
|
for (; ; ) {
|
|
585
596
|
try {
|
|
586
597
|
const real = await fsp2.realpath(probe);
|
|
587
|
-
if (!isInside(realTarget, real))
|
|
598
|
+
if (!isInside(realTarget, real))
|
|
599
|
+
throw new Error("path resolves through a symlink outside checkpoint target");
|
|
588
600
|
return output;
|
|
589
601
|
} catch (err) {
|
|
590
602
|
if (err.code !== "ENOENT") throw err;
|
|
@@ -596,7 +608,7 @@ var SessionCheckpointCas = class {
|
|
|
596
608
|
}
|
|
597
609
|
};
|
|
598
610
|
function defaultRunGit(args, cwd) {
|
|
599
|
-
return new Promise((
|
|
611
|
+
return new Promise((resolve5) => {
|
|
600
612
|
const stdoutChunks = [];
|
|
601
613
|
const stderrChunks = [];
|
|
602
614
|
let stdoutBytes = 0;
|
|
@@ -639,8 +651,8 @@ function defaultRunGit(args, cwd) {
|
|
|
639
651
|
stdoutTruncated,
|
|
640
652
|
stderrTruncated
|
|
641
653
|
});
|
|
642
|
-
child.on("error", (err) =>
|
|
643
|
-
child.on("close", (code) =>
|
|
654
|
+
child.on("error", (err) => resolve5(result(1, err.message)));
|
|
655
|
+
child.on("close", (code) => resolve5(result(code ?? 1)));
|
|
644
656
|
});
|
|
645
657
|
}
|
|
646
658
|
|
|
@@ -649,19 +661,22 @@ function sanitizeModel(model) {
|
|
|
649
661
|
return model.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
650
662
|
}
|
|
651
663
|
function generateSessionId(startedAt, _model) {
|
|
652
|
-
const
|
|
653
|
-
const
|
|
664
|
+
const parsedTime = Date.parse(startedAt);
|
|
665
|
+
const isValidDate = !Number.isNaN(parsedTime) && /^\d{4}-\d{2}-\d{2}/.test(startedAt);
|
|
666
|
+
const seedTime = isValidDate ? parsedTime : Date.now();
|
|
667
|
+
const date = isValidDate ? startedAt.slice(0, 10) : new Date(seedTime).toISOString().slice(0, 10);
|
|
654
668
|
return `${date}/sess_${ulid(seedTime)}`;
|
|
655
669
|
}
|
|
656
670
|
|
|
657
671
|
// src/storage/session-id-resolver.ts
|
|
658
672
|
import * as path2 from "node:path";
|
|
659
673
|
function resolveSessionId(query, candidateIds) {
|
|
660
|
-
const
|
|
674
|
+
const trimmed = query.trim();
|
|
675
|
+
const normalized = trimmed.replace(/\\/g, "/");
|
|
661
676
|
if (!normalized) return { status: "missing", query: normalized };
|
|
662
|
-
const uniqueIds = [...new Set(candidateIds)];
|
|
677
|
+
const uniqueIds = [...new Set(candidateIds.map((id) => id.replace(/\\/g, "/")))];
|
|
663
678
|
if (uniqueIds.includes(normalized)) return { status: "resolved", id: normalized };
|
|
664
|
-
const leaf = (id) => path2.posix.basename(id
|
|
679
|
+
const leaf = (id) => path2.posix.basename(id);
|
|
665
680
|
const exactLeafMatches = uniqueIds.filter((id) => leaf(id) === normalized);
|
|
666
681
|
if (exactLeafMatches.length === 1) {
|
|
667
682
|
return { status: "resolved", id: exactLeafMatches[0] };
|
|
@@ -680,7 +695,7 @@ function resolveSessionId(query, candidateIds) {
|
|
|
680
695
|
if (prefixMatches.length > 1) {
|
|
681
696
|
return { status: "ambiguous", query: normalized, candidates: prefixMatches.sort() };
|
|
682
697
|
}
|
|
683
|
-
return { status: "missing", query:
|
|
698
|
+
return { status: "missing", query: trimmed };
|
|
684
699
|
}
|
|
685
700
|
function sessionIdResolutionError(resolution) {
|
|
686
701
|
if (resolution.status === "ambiguous") {
|
|
@@ -752,7 +767,10 @@ function stripTerminalControls(value) {
|
|
|
752
767
|
return out;
|
|
753
768
|
}
|
|
754
769
|
function sessionContentText(content) {
|
|
755
|
-
|
|
770
|
+
if (!content) return "";
|
|
771
|
+
const text = typeof content === "string" ? content : Array.isArray(content) ? content.filter(
|
|
772
|
+
(b) => Boolean(b && b.type === "text" && typeof b.text === "string")
|
|
773
|
+
).map((b) => b.text).join(" ") : String(content);
|
|
756
774
|
return stripTerminalControls(text).trim().replace(/\s+/g, " ");
|
|
757
775
|
}
|
|
758
776
|
function sessionContentPreview(content, maxLength = 160) {
|
|
@@ -765,7 +783,7 @@ function userInputTitle(content) {
|
|
|
765
783
|
|
|
766
784
|
// src/storage/session-read-scrubber.ts
|
|
767
785
|
function scrubPersistedSessionEvent(event, scrubber) {
|
|
768
|
-
return scrubber.scrubObject(event);
|
|
786
|
+
return scrubber.scrubObjectShared ? scrubber.scrubObjectShared(event) : scrubber.scrubObject(event);
|
|
769
787
|
}
|
|
770
788
|
function scrubPersistedSessionData(data, scrubber) {
|
|
771
789
|
return scrubber.scrubObject(data);
|
|
@@ -793,13 +811,6 @@ import { createInterface } from "node:readline";
|
|
|
793
811
|
// src/storage/session-store/directory-session-files.ts
|
|
794
812
|
import * as fsp3 from "node:fs/promises";
|
|
795
813
|
import * as path3 from "node:path";
|
|
796
|
-
|
|
797
|
-
// src/storage/session-store/directory-scan.ts
|
|
798
|
-
function shouldSkipSessionDirectoryEntry(name) {
|
|
799
|
-
return name.startsWith(".") && name !== ".wrongstack" || name === "shared" || name === "subagents" || name === "attachments";
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// src/storage/session-store/directory-session-files.ts
|
|
803
814
|
function sessionIdForFile(prefix, name) {
|
|
804
815
|
const base = name.replace(/\.jsonl$/, "");
|
|
805
816
|
return prefix ? `${prefix}/${base}` : base;
|
|
@@ -1099,6 +1110,68 @@ var SessionRecovery = class _SessionRecovery {
|
|
|
1099
1110
|
* recent crash first.
|
|
1100
1111
|
*/
|
|
1101
1112
|
async listResumable() {
|
|
1113
|
+
const out = [];
|
|
1114
|
+
for (const { sessionId } of await this.collectTranscripts()) {
|
|
1115
|
+
const stale = await this.detectStaleExact(sessionId);
|
|
1116
|
+
if (stale) out.push(stale);
|
|
1117
|
+
}
|
|
1118
|
+
return out.sort((a, b) => b.lastEventTs.localeCompare(a.lastEventTs));
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* List every session whose journal has no trailing `session_end`, newest
|
|
1122
|
+
* file first. This is the read side of "recovery" as a user means it: the
|
|
1123
|
+
* conversations a crash, a `kill`, or a closed laptop lid left hanging.
|
|
1124
|
+
*
|
|
1125
|
+
* Ordering is by file mtime, not by boundary timestamp: a journal with no
|
|
1126
|
+
* lifecycle boundary at all (a session that never completed a turn) has no
|
|
1127
|
+
* timestamp to sort on, and mtime is the one key every candidate has.
|
|
1128
|
+
*
|
|
1129
|
+
* @param options.limit Stop after this many unclosed sessions are found.
|
|
1130
|
+
* Candidates are examined newest-mtime-first, so the cap keeps the common
|
|
1131
|
+
* "give me the last one" call to a couple of tail reads instead of a scan
|
|
1132
|
+
* of every transcript the project ever wrote.
|
|
1133
|
+
*/
|
|
1134
|
+
async listUnclosed(options = {}) {
|
|
1135
|
+
const limit = options.limit ?? Number.POSITIVE_INFINITY;
|
|
1136
|
+
if (limit <= 0) return [];
|
|
1137
|
+
const candidates = await this.collectTranscripts();
|
|
1138
|
+
candidates.sort((a, b) => b.modifiedAt - a.modifiedAt);
|
|
1139
|
+
const out = [];
|
|
1140
|
+
for (const candidate of candidates) {
|
|
1141
|
+
if (out.length >= limit) break;
|
|
1142
|
+
const unclosed = await this.detectUnclosedExact(candidate);
|
|
1143
|
+
if (unclosed) out.push(unclosed);
|
|
1144
|
+
}
|
|
1145
|
+
return out;
|
|
1146
|
+
}
|
|
1147
|
+
async detectUnclosedExact(candidate) {
|
|
1148
|
+
if (candidate.size === 0) return null;
|
|
1149
|
+
let boundary;
|
|
1150
|
+
try {
|
|
1151
|
+
boundary = await scanLatestLifecycleBoundary(candidate.path, candidate.size, {
|
|
1152
|
+
countEvents: false
|
|
1153
|
+
}).then((scan) => scan?.boundary ?? null);
|
|
1154
|
+
} catch {
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
if (boundary?.type === "session_end") return null;
|
|
1158
|
+
return {
|
|
1159
|
+
sessionId: candidate.sessionId,
|
|
1160
|
+
path: candidate.path,
|
|
1161
|
+
lastBoundary: boundary ? boundary.type : null,
|
|
1162
|
+
lastEventTs: boundary ? boundary.ts : new Date(candidate.modifiedAt).toISOString(),
|
|
1163
|
+
stale: boundary?.type === "in_flight_start",
|
|
1164
|
+
modifiedAt: candidate.modifiedAt
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Every session transcript under `dir`, with the stat both scanners need.
|
|
1169
|
+
*
|
|
1170
|
+
* Modern sessions live inside date-shard subdirectories
|
|
1171
|
+
* ("2026-06-11/<base>.jsonl"); legacy/flat sessions sit at the root. Scan
|
|
1172
|
+
* both — a root-only scan silently misses every modern crash.
|
|
1173
|
+
*/
|
|
1174
|
+
async collectTranscripts() {
|
|
1102
1175
|
const out = [];
|
|
1103
1176
|
const collect = async (dir, prefix, depth) => {
|
|
1104
1177
|
let entries;
|
|
@@ -1120,12 +1193,18 @@ var SessionRecovery = class _SessionRecovery {
|
|
|
1120
1193
|
if (!entry.isFile() || !isSessionTranscriptFileName(entry.name)) continue;
|
|
1121
1194
|
const base = entry.name.slice(0, -".jsonl".length);
|
|
1122
1195
|
const sessionId = prefix ? `${prefix}/${base}` : base;
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1196
|
+
const fp = path4.join(dir, entry.name);
|
|
1197
|
+
let stat11;
|
|
1198
|
+
try {
|
|
1199
|
+
stat11 = await fs.stat(fp);
|
|
1200
|
+
} catch {
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
out.push({ sessionId, path: fp, size: stat11.size, modifiedAt: stat11.mtimeMs });
|
|
1125
1204
|
}
|
|
1126
1205
|
};
|
|
1127
1206
|
await collect(this.dir, "", 0);
|
|
1128
|
-
return out
|
|
1207
|
+
return out;
|
|
1129
1208
|
}
|
|
1130
1209
|
// ── Internals ──────────────────────────────────────────────────────────
|
|
1131
1210
|
filePath(sessionId) {
|
|
@@ -1149,7 +1228,8 @@ function hasNonWhitespace(line) {
|
|
|
1149
1228
|
}
|
|
1150
1229
|
return false;
|
|
1151
1230
|
}
|
|
1152
|
-
async function scanLatestLifecycleBoundary(filePath, size) {
|
|
1231
|
+
async function scanLatestLifecycleBoundary(filePath, size, options = {}) {
|
|
1232
|
+
const countEvents = options.countEvents !== false;
|
|
1153
1233
|
const CHUNK_SIZE2 = 64 * 1024;
|
|
1154
1234
|
const handle = await fs.open(filePath, "r");
|
|
1155
1235
|
let position = size;
|
|
@@ -1176,7 +1256,7 @@ async function scanLatestLifecycleBoundary(filePath, size) {
|
|
|
1176
1256
|
lineEnd = i;
|
|
1177
1257
|
}
|
|
1178
1258
|
laterLineFragment = data.subarray(0, lineEnd);
|
|
1179
|
-
if (latestBoundary && latestBoundary.type !== "in_flight_start") {
|
|
1259
|
+
if (latestBoundary && (!countEvents || latestBoundary.type !== "in_flight_start")) {
|
|
1180
1260
|
return { boundary: latestBoundary, eventCount };
|
|
1181
1261
|
}
|
|
1182
1262
|
}
|
|
@@ -1201,6 +1281,19 @@ import * as path5 from "node:path";
|
|
|
1201
1281
|
import { createReadStream as createReadStream2 } from "node:fs";
|
|
1202
1282
|
import * as fsp4 from "node:fs/promises";
|
|
1203
1283
|
import { createInterface as createInterface2 } from "node:readline";
|
|
1284
|
+
|
|
1285
|
+
// src/storage/session-outcome.ts
|
|
1286
|
+
function resolveSessionOutcome(lastEventType, hadSessionError) {
|
|
1287
|
+
if (lastEventType === "session_end") return "completed";
|
|
1288
|
+
if (lastEventType === "in_flight_start") return "aborted";
|
|
1289
|
+
if (hadSessionError) return "error";
|
|
1290
|
+
return void 0;
|
|
1291
|
+
}
|
|
1292
|
+
function isSessionErrorEvent(event) {
|
|
1293
|
+
return event.type === "error" || event.type === "provider_error";
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// src/storage/session-summary-tracker.ts
|
|
1204
1297
|
var SessionSummaryTracker = class {
|
|
1205
1298
|
id;
|
|
1206
1299
|
startedAt;
|
|
@@ -1220,7 +1313,14 @@ var SessionSummaryTracker = class {
|
|
|
1220
1313
|
messageCount = 0;
|
|
1221
1314
|
lastUserMessage;
|
|
1222
1315
|
lastActivityAt;
|
|
1223
|
-
|
|
1316
|
+
/**
|
|
1317
|
+
* Whether an `error`/`provider_error` was seen. Not a latched outcome — see
|
|
1318
|
+
* {@link resolveSessionOutcome} for why a trailing terminal marker outranks
|
|
1319
|
+
* it, and why a failed tool_result is deliberately not counted here.
|
|
1320
|
+
*/
|
|
1321
|
+
hadSessionError = false;
|
|
1322
|
+
/** Type of the newest observed event; the terminal-marker signal. */
|
|
1323
|
+
lastEventType;
|
|
1224
1324
|
openToolUses = /* @__PURE__ */ new Set();
|
|
1225
1325
|
constructor(opts) {
|
|
1226
1326
|
this.id = opts.id;
|
|
@@ -1246,7 +1346,7 @@ var SessionSummaryTracker = class {
|
|
|
1246
1346
|
this.messageCount = this.summary.messageCount ?? 0;
|
|
1247
1347
|
this.lastUserMessage = this.summary.lastUserMessage;
|
|
1248
1348
|
this.lastActivityAt = this.summary.lastActivityAt ?? this.summary.endedAt ?? opts.startedAt;
|
|
1249
|
-
this.
|
|
1349
|
+
this.hadSessionError = this.resumed ? false : this.summary.outcome === "error";
|
|
1250
1350
|
}
|
|
1251
1351
|
get currentSummary() {
|
|
1252
1352
|
return this.summary;
|
|
@@ -1271,13 +1371,17 @@ var SessionSummaryTracker = class {
|
|
|
1271
1371
|
compactionCount: this.compactionCount > 0 ? this.compactionCount : void 0,
|
|
1272
1372
|
toolBreakdown: { ...this.toolBreakdown },
|
|
1273
1373
|
lastActivityAt: this.lastActivityAt,
|
|
1274
|
-
...
|
|
1374
|
+
...(() => {
|
|
1375
|
+
const outcome = resolveSessionOutcome(this.lastEventType, this.hadSessionError);
|
|
1376
|
+
return outcome !== void 0 ? { outcome } : {};
|
|
1377
|
+
})()
|
|
1275
1378
|
};
|
|
1276
1379
|
}
|
|
1277
1380
|
get pendingToolUses() {
|
|
1278
1381
|
return Array.from(this.openToolUses);
|
|
1279
1382
|
}
|
|
1280
1383
|
observe(event) {
|
|
1384
|
+
this.lastEventType = event.type;
|
|
1281
1385
|
const eventActivityMs = Date.parse(event.ts);
|
|
1282
1386
|
const lastActivityMs = Date.parse(this.lastActivityAt);
|
|
1283
1387
|
if (Number.isFinite(eventActivityMs) && (!Number.isFinite(lastActivityMs) || eventActivityMs > lastActivityMs)) {
|
|
@@ -1295,17 +1399,14 @@ var SessionSummaryTracker = class {
|
|
|
1295
1399
|
this.toolBreakdown[event.name] = (this.toolBreakdown[event.name] ?? 0) + 1;
|
|
1296
1400
|
} else if (event.type === "tool_result") {
|
|
1297
1401
|
this.openToolUses.delete(event.id);
|
|
1298
|
-
if (event.isError)
|
|
1299
|
-
this.toolErrorCount++;
|
|
1300
|
-
this.outcome = "error";
|
|
1301
|
-
}
|
|
1402
|
+
if (event.isError) this.toolErrorCount++;
|
|
1302
1403
|
} else if (event.type === "file_snapshot") {
|
|
1303
1404
|
this.fileChangeCount += event.files.length;
|
|
1304
1405
|
} else if (event.type === "compaction") {
|
|
1305
1406
|
this.compactionCount++;
|
|
1306
1407
|
}
|
|
1307
|
-
if (event
|
|
1308
|
-
this.
|
|
1408
|
+
if (isSessionErrorEvent(event)) {
|
|
1409
|
+
this.hadSessionError = true;
|
|
1309
1410
|
}
|
|
1310
1411
|
if (event.type === "user_input") {
|
|
1311
1412
|
if (this.summary.title === "(empty session)") {
|
|
@@ -1315,14 +1416,21 @@ var SessionSummaryTracker = class {
|
|
|
1315
1416
|
this.messageCount++;
|
|
1316
1417
|
} else if (event.type === "llm_response") {
|
|
1317
1418
|
this.messageCount++;
|
|
1318
|
-
this.tokenIn += event.usage
|
|
1419
|
+
this.tokenIn += effectiveInputTokens(event.usage);
|
|
1319
1420
|
this.tokenOut += event.usage.output;
|
|
1320
1421
|
this.summary = {
|
|
1321
1422
|
...this.summary,
|
|
1322
|
-
tokenTotal: this.baseTokenTotal + this.tokenIn + this.tokenOut
|
|
1423
|
+
tokenTotal: this.baseTokenTotal + this.tokenIn + this.tokenOut,
|
|
1424
|
+
// Track the routed identity, not the one the session opened with. A
|
|
1425
|
+
// mid-session /model switch or a fallback rotation leaves the
|
|
1426
|
+
// create-time metadata stale, and this summary is what every session
|
|
1427
|
+
// listing, picker and catalog row shows. Older journals have no
|
|
1428
|
+
// model/provider on llm_response — those keep the create-time value.
|
|
1429
|
+
...event.model !== void 0 ? { model: event.model } : {},
|
|
1430
|
+
...event.provider !== void 0 ? { provider: event.provider } : {}
|
|
1323
1431
|
};
|
|
1324
1432
|
} else if (event.type === "session_end") {
|
|
1325
|
-
const total = event.usage
|
|
1433
|
+
const total = totalUsageTokens(event.usage);
|
|
1326
1434
|
if (total > 0) {
|
|
1327
1435
|
this.summary = {
|
|
1328
1436
|
...this.summary,
|
|
@@ -1346,7 +1454,8 @@ var SessionSummaryTracker = class {
|
|
|
1346
1454
|
this.messageCount = 0;
|
|
1347
1455
|
this.lastUserMessage = void 0;
|
|
1348
1456
|
this.lastActivityAt = this.startedAt;
|
|
1349
|
-
this.
|
|
1457
|
+
this.hadSessionError = false;
|
|
1458
|
+
this.lastEventType = void 0;
|
|
1350
1459
|
this.openToolUses = /* @__PURE__ */ new Set();
|
|
1351
1460
|
this.tokenIn = 0;
|
|
1352
1461
|
this.tokenOut = 0;
|
|
@@ -1399,7 +1508,7 @@ var SessionSummaryTracker = class {
|
|
|
1399
1508
|
fileChangeCount: this.fileChangeCount,
|
|
1400
1509
|
compactionCount: this.compactionCount > 0 ? this.compactionCount : void 0,
|
|
1401
1510
|
toolBreakdown: { ...this.toolBreakdown },
|
|
1402
|
-
outcome: this.
|
|
1511
|
+
outcome: resolveSessionOutcome(this.lastEventType, this.hadSessionError)
|
|
1403
1512
|
};
|
|
1404
1513
|
const resolvedName = this.resolveNameCb ? await this.resolveNameCb().catch(() => null) : null;
|
|
1405
1514
|
if (resolvedName !== null) {
|
|
@@ -1425,7 +1534,8 @@ var SessionSummaryTracker = class {
|
|
|
1425
1534
|
this.messageCount = 0;
|
|
1426
1535
|
this.lastUserMessage = void 0;
|
|
1427
1536
|
this.lastActivityAt = resetAt;
|
|
1428
|
-
this.
|
|
1537
|
+
this.hadSessionError = false;
|
|
1538
|
+
this.lastEventType = void 0;
|
|
1429
1539
|
this.openToolUses = /* @__PURE__ */ new Set();
|
|
1430
1540
|
this.summary = {
|
|
1431
1541
|
id: this.id,
|
|
@@ -1457,6 +1567,16 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
1457
1567
|
static FLUSH_SIZE = 50;
|
|
1458
1568
|
static WRITE_BUFFER_MAX_EVENTS = 2e3;
|
|
1459
1569
|
static WRITE_BUFFER_MAX_BYTES = 16 * 1024 * 1024;
|
|
1570
|
+
/**
|
|
1571
|
+
* Absolute ceiling for ONE event, even via the empty-buffer exemption in
|
|
1572
|
+
* `push`. That exemption exists so a legitimately huge user prompt is
|
|
1573
|
+
* persisted instead of silently dropped (its bytes are already resident —
|
|
1574
|
+
* accepting it costs one reference, not a copy); without a ceiling an
|
|
1575
|
+
* unbounded payload could pin memory and stall the event loop on
|
|
1576
|
+
* stringify. 64MiB is 4x the retention budget and far above any real
|
|
1577
|
+
* prompt, so it only fires on pathological input.
|
|
1578
|
+
*/
|
|
1579
|
+
static MAX_SINGLE_EVENT_BYTES = 64 * 1024 * 1024;
|
|
1460
1580
|
writeBuffer = [];
|
|
1461
1581
|
writeBufferBytes = 0;
|
|
1462
1582
|
flushTimer = null;
|
|
@@ -1464,6 +1584,8 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
1464
1584
|
lastBufferOverflowWarnAt = 0;
|
|
1465
1585
|
appendFailCount = 0;
|
|
1466
1586
|
lastAppendWarnAt = 0;
|
|
1587
|
+
invalidEventCount = 0;
|
|
1588
|
+
lastInvalidEventWarnAt = 0;
|
|
1467
1589
|
writeChain = Promise.resolve();
|
|
1468
1590
|
flushPromise = null;
|
|
1469
1591
|
/**
|
|
@@ -1482,12 +1604,31 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
1482
1604
|
try {
|
|
1483
1605
|
return Buffer.byteLength(JSON.stringify(event), "utf8") + 1;
|
|
1484
1606
|
} catch {
|
|
1485
|
-
return
|
|
1607
|
+
return Number.NaN;
|
|
1486
1608
|
}
|
|
1487
1609
|
}
|
|
1488
1610
|
push(event) {
|
|
1489
1611
|
const bytes = this.eventBytes(event);
|
|
1490
|
-
if (
|
|
1612
|
+
if (!Number.isFinite(bytes)) {
|
|
1613
|
+
this.invalidEventCount++;
|
|
1614
|
+
const now = Date.now();
|
|
1615
|
+
if (now - this.lastInvalidEventWarnAt > 5e3) {
|
|
1616
|
+
this.lastInvalidEventWarnAt = now;
|
|
1617
|
+
console.warn(
|
|
1618
|
+
JSON.stringify({
|
|
1619
|
+
level: "error",
|
|
1620
|
+
event: "session.buffer_push_invalid",
|
|
1621
|
+
sessionId: this.opts.sessionId,
|
|
1622
|
+
reason: "event is not JSON-serializable",
|
|
1623
|
+
droppedEvents: this.invalidEventCount,
|
|
1624
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1625
|
+
})
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
return false;
|
|
1629
|
+
}
|
|
1630
|
+
const oversizedSingleEvent = bytes > _SessionWriteBuffer.WRITE_BUFFER_MAX_BYTES && bytes <= _SessionWriteBuffer.MAX_SINGLE_EVENT_BYTES && this.writeBuffer.length === 0;
|
|
1631
|
+
if (this.writeBuffer.length >= _SessionWriteBuffer.WRITE_BUFFER_MAX_EVENTS || !oversizedSingleEvent && (bytes > _SessionWriteBuffer.WRITE_BUFFER_MAX_BYTES || this.writeBufferBytes + bytes > _SessionWriteBuffer.WRITE_BUFFER_MAX_BYTES)) {
|
|
1491
1632
|
this.bufferOverflowCount++;
|
|
1492
1633
|
const now = Date.now();
|
|
1493
1634
|
if (now - this.lastBufferOverflowWarnAt > 5e3) {
|
|
@@ -1520,7 +1661,7 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
1520
1661
|
flight.started = true;
|
|
1521
1662
|
if (flight.stolen) return;
|
|
1522
1663
|
try {
|
|
1523
|
-
|
|
1664
|
+
await this.opts.getHandle().appendFile(flight.data, "utf8");
|
|
1524
1665
|
} catch (err) {
|
|
1525
1666
|
if (isClosedHandleError(err)) {
|
|
1526
1667
|
const reloaded = await fsp5.open(this.opts.filePath, "a", 384);
|
|
@@ -1644,6 +1785,12 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
1644
1785
|
/**
|
|
1645
1786
|
* Last-gasp synchronous append (SIGKILL/SIGTERM traps, `process.on('exit')`).
|
|
1646
1787
|
*
|
|
1788
|
+
* If an async append is actively writing when teardown arrives, flushSync
|
|
1789
|
+
* must not open a second append descriptor and race it. It defers with a
|
|
1790
|
+
* structured `session.flush_sync_deferred` warning, leaving the buffer
|
|
1791
|
+
* untouched for the already-running flush loop to drain if the process
|
|
1792
|
+
* survives.
|
|
1793
|
+
*
|
|
1647
1794
|
* Failure contract: nothing is discarded before the write is known to have
|
|
1648
1795
|
* landed. Buffered events stay in `writeBuffer` — it is cleared only after
|
|
1649
1796
|
* `fsyncSync` returns — and a stolen in-flight batch is handed back to the
|
|
@@ -1658,6 +1805,21 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
1658
1805
|
const chunks = [];
|
|
1659
1806
|
const flight = this.inFlight;
|
|
1660
1807
|
const stole = flight !== null && !flight.started && !flight.stolen;
|
|
1808
|
+
if (flight !== null && flight.started && !flight.stolen) {
|
|
1809
|
+
console.warn(
|
|
1810
|
+
JSON.stringify({
|
|
1811
|
+
level: "error",
|
|
1812
|
+
event: "session.flush_sync_deferred",
|
|
1813
|
+
sessionId: this.opts.sessionId,
|
|
1814
|
+
filePath: this.opts.filePath,
|
|
1815
|
+
message: "in-flight async append already started; sync append deferred to avoid write race",
|
|
1816
|
+
pendingEvents: this.writeBuffer.length,
|
|
1817
|
+
pendingBytes: this.writeBufferBytes,
|
|
1818
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1819
|
+
})
|
|
1820
|
+
);
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1661
1823
|
if (stole && flight) {
|
|
1662
1824
|
flight.stolen = true;
|
|
1663
1825
|
chunks.push(flight.data);
|
|
@@ -1710,7 +1872,7 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
1710
1872
|
this.lastBufferOverflowWarnAt = 0;
|
|
1711
1873
|
}
|
|
1712
1874
|
shouldFlushNow() {
|
|
1713
|
-
return this.writeBuffer.length >= _SessionWriteBuffer.FLUSH_SIZE;
|
|
1875
|
+
return this.writeBuffer.length >= _SessionWriteBuffer.FLUSH_SIZE || this.writeBufferBytes >= _SessionWriteBuffer.WRITE_BUFFER_MAX_BYTES;
|
|
1714
1876
|
}
|
|
1715
1877
|
};
|
|
1716
1878
|
|
|
@@ -1739,6 +1901,18 @@ function scrubSessionWriterEvent(event, secretScrubber) {
|
|
|
1739
1901
|
if (event.type === "llm_response") {
|
|
1740
1902
|
return { ...event, content: secretScrubber.scrubObject(event.content) };
|
|
1741
1903
|
}
|
|
1904
|
+
if (event.type === "tool_use") {
|
|
1905
|
+
return { ...event, input: secretScrubber.scrubObject(event.input) };
|
|
1906
|
+
}
|
|
1907
|
+
if (event.type === "tool_call_start") {
|
|
1908
|
+
return { ...event, input: secretScrubber.scrubObject(event.input) };
|
|
1909
|
+
}
|
|
1910
|
+
if (event.type === "tool_result") {
|
|
1911
|
+
return {
|
|
1912
|
+
...event,
|
|
1913
|
+
content: typeof event.content === "string" ? secretScrubber.scrub(event.content) : secretScrubber.scrubObject(event.content)
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1742
1916
|
if (event.type === "file_snapshot") {
|
|
1743
1917
|
return {
|
|
1744
1918
|
...event,
|
|
@@ -1749,6 +1923,9 @@ function scrubSessionWriterEvent(event, secretScrubber) {
|
|
|
1749
1923
|
}))
|
|
1750
1924
|
};
|
|
1751
1925
|
}
|
|
1926
|
+
if (event.type === "side_effect") {
|
|
1927
|
+
return secretScrubber.scrubObject(event);
|
|
1928
|
+
}
|
|
1752
1929
|
return event;
|
|
1753
1930
|
}
|
|
1754
1931
|
|
|
@@ -1761,6 +1938,8 @@ async function findSessionCheckpointTruncatePlan(filePath, targetPromptIndex) {
|
|
|
1761
1938
|
let pendingLine = Buffer.alloc(0);
|
|
1762
1939
|
let checkpointByteOffset = -1;
|
|
1763
1940
|
let removedCount = 0;
|
|
1941
|
+
const removedSubagentTranscriptPaths = /* @__PURE__ */ new Set();
|
|
1942
|
+
const retainedSubagentTranscriptPaths = /* @__PURE__ */ new Set();
|
|
1764
1943
|
try {
|
|
1765
1944
|
fd = await fsp6.open(filePath, "r", 384);
|
|
1766
1945
|
while (true) {
|
|
@@ -1775,15 +1954,20 @@ async function findSessionCheckpointTruncatePlan(filePath, targetPromptIndex) {
|
|
|
1775
1954
|
const idx = chunk.indexOf("\n", chunkPos);
|
|
1776
1955
|
if (idx === -1) break;
|
|
1777
1956
|
const lineStartOffset = chunkFileOffset + chunkPos;
|
|
1957
|
+
const lineBytes = chunk.subarray(chunkPos, idx);
|
|
1778
1958
|
if (checkpointByteOffset !== -1) {
|
|
1779
1959
|
removedCount++;
|
|
1960
|
+
collectSubagentTranscriptPath(lineBytes, removedSubagentTranscriptPaths);
|
|
1780
1961
|
} else {
|
|
1781
|
-
const lineBytes = chunk.subarray(chunkPos, idx);
|
|
1782
1962
|
const line = new TextDecoder("utf-8", { fatal: false }).decode(lineBytes);
|
|
1783
1963
|
if (line.trim()) {
|
|
1784
1964
|
try {
|
|
1785
1965
|
const event = JSON.parse(line);
|
|
1786
|
-
if (event.type === "
|
|
1966
|
+
if (event.type === "agent_session_linked") {
|
|
1967
|
+
if (typeof event.transcriptPath === "string" && event.transcriptPath.trim()) {
|
|
1968
|
+
retainedSubagentTranscriptPaths.add(event.transcriptPath);
|
|
1969
|
+
}
|
|
1970
|
+
} else if (event.type === "checkpoint") {
|
|
1787
1971
|
if (event.promptIndex === targetPromptIndex) {
|
|
1788
1972
|
checkpointByteOffset = lineStartOffset;
|
|
1789
1973
|
} else if (event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
@@ -1803,7 +1987,23 @@ async function findSessionCheckpointTruncatePlan(filePath, targetPromptIndex) {
|
|
|
1803
1987
|
await fd?.close();
|
|
1804
1988
|
}
|
|
1805
1989
|
if (checkpointByteOffset === -1) return null;
|
|
1806
|
-
return {
|
|
1990
|
+
return {
|
|
1991
|
+
checkpointByteOffset,
|
|
1992
|
+
removedCount,
|
|
1993
|
+
removedSubagentTranscriptPaths: [...removedSubagentTranscriptPaths].filter((transcriptPath) => !retainedSubagentTranscriptPaths.has(transcriptPath)).sort()
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
function collectSubagentTranscriptPath(lineBytes, out) {
|
|
1997
|
+
const line = new TextDecoder("utf-8", { fatal: false }).decode(lineBytes);
|
|
1998
|
+
if (!line.trim()) return;
|
|
1999
|
+
try {
|
|
2000
|
+
const event = JSON.parse(line);
|
|
2001
|
+
if (event.type !== "agent_session_linked") return;
|
|
2002
|
+
if (typeof event.transcriptPath === "string" && event.transcriptPath.trim()) {
|
|
2003
|
+
out.add(event.transcriptPath);
|
|
2004
|
+
}
|
|
2005
|
+
} catch {
|
|
2006
|
+
}
|
|
1807
2007
|
}
|
|
1808
2008
|
async function rewriteSessionToCheckpoint(filePath, checkpointByteOffset) {
|
|
1809
2009
|
const tmpPath = `${filePath}.rewind.tmp`;
|
|
@@ -1862,6 +2062,7 @@ var CRITICAL_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
|
1862
2062
|
"user_input",
|
|
1863
2063
|
"llm_response",
|
|
1864
2064
|
"checkpoint",
|
|
2065
|
+
"rewound",
|
|
1865
2066
|
"in_flight_start",
|
|
1866
2067
|
"in_flight_end"
|
|
1867
2068
|
]);
|
|
@@ -1877,6 +2078,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
1877
2078
|
this.meta = meta;
|
|
1878
2079
|
this.events = events;
|
|
1879
2080
|
this.resumed = opts.resumed ?? false;
|
|
2081
|
+
this.lifecyclePreambleTs = this.resumed ? (/* @__PURE__ */ new Date()).toISOString() : startedAt;
|
|
1880
2082
|
this.manifestFile = opts.dir ? path5.join(opts.dir, `${path5.basename(id)}.summary.json`) : "";
|
|
1881
2083
|
this.filePath = opts.filePath ?? "";
|
|
1882
2084
|
this.secretScrubber = opts.secretScrubber;
|
|
@@ -1932,6 +2134,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
1932
2134
|
return this.initPromise;
|
|
1933
2135
|
}
|
|
1934
2136
|
resumed;
|
|
2137
|
+
lifecyclePreambleTs;
|
|
1935
2138
|
secretScrubber;
|
|
1936
2139
|
checkpointCas;
|
|
1937
2140
|
/** Mutable slot for onAppend callback — constructor opts seed it, setOnAppend replaces it. */
|
|
@@ -2013,7 +2216,18 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
2013
2216
|
`)).then(() => {
|
|
2014
2217
|
if (!critical) return;
|
|
2015
2218
|
return this.handle.datasync().catch(() => void 0);
|
|
2016
|
-
}).catch(() =>
|
|
2219
|
+
}).catch((err) => {
|
|
2220
|
+
console.warn(
|
|
2221
|
+
JSON.stringify({
|
|
2222
|
+
level: "error",
|
|
2223
|
+
event: "session.sync_journal_write_failed",
|
|
2224
|
+
sessionId: this.id,
|
|
2225
|
+
eventType: appendEvent.type,
|
|
2226
|
+
message: toErrorMessage(err),
|
|
2227
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2228
|
+
})
|
|
2229
|
+
);
|
|
2230
|
+
});
|
|
2017
2231
|
});
|
|
2018
2232
|
} else if (critical || this.buffer.shouldFlushNow()) {
|
|
2019
2233
|
this.buffer.cancelTimer();
|
|
@@ -2082,7 +2296,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
2082
2296
|
async writeSessionStartLazy() {
|
|
2083
2297
|
this.buffer.push({
|
|
2084
2298
|
type: this.resumed ? "session_resumed" : "session_start",
|
|
2085
|
-
ts: this.
|
|
2299
|
+
ts: this.lifecyclePreambleTs,
|
|
2086
2300
|
id: this.id,
|
|
2087
2301
|
model: this.meta.model ?? "unknown",
|
|
2088
2302
|
provider: this.meta.provider ?? "unknown"
|
|
@@ -2188,7 +2402,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
2188
2402
|
`)).then(() => {
|
|
2189
2403
|
if (!isCriticalEvent(scrubbed)) return;
|
|
2190
2404
|
return this.handle.datasync().catch(() => void 0);
|
|
2191
|
-
})
|
|
2405
|
+
});
|
|
2192
2406
|
}
|
|
2193
2407
|
}
|
|
2194
2408
|
if (isCriticalEvent(scrubbed) || this.buffer.shouldFlushNow()) {
|
|
@@ -2220,7 +2434,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
2220
2434
|
`)).then(() => {
|
|
2221
2435
|
if (!isCriticalEvent(scrubbed)) return;
|
|
2222
2436
|
return this.handle.datasync().catch(() => void 0);
|
|
2223
|
-
})
|
|
2437
|
+
});
|
|
2224
2438
|
}
|
|
2225
2439
|
}
|
|
2226
2440
|
scrubbedBatch.push(scrubbed);
|
|
@@ -2453,6 +2667,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
2453
2667
|
}
|
|
2454
2668
|
try {
|
|
2455
2669
|
await rewriteSessionToCheckpoint(this.filePath, plan.checkpointByteOffset);
|
|
2670
|
+
await this.deleteRewoundSubagentTranscripts(plan.removedSubagentTranscriptPaths);
|
|
2456
2671
|
this.handle = await fsp7.open(this.filePath, "a", 384);
|
|
2457
2672
|
} catch (err) {
|
|
2458
2673
|
this.handle = await fsp7.open(this.filePath, "a", 384).catch(() => this.handle);
|
|
@@ -2476,6 +2691,42 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
2476
2691
|
});
|
|
2477
2692
|
return plan.removedCount;
|
|
2478
2693
|
}
|
|
2694
|
+
async deleteRewoundSubagentTranscripts(transcriptPaths) {
|
|
2695
|
+
if (transcriptPaths.length === 0) return;
|
|
2696
|
+
const sessionsRoot = this.id.includes("/") ? path5.dirname(path5.dirname(this.filePath)) : path5.dirname(this.filePath);
|
|
2697
|
+
const allowedRoot = path5.join(sessionsRoot, "subagents");
|
|
2698
|
+
const realAllowedRoot = await fsp7.realpath(allowedRoot).catch(() => null);
|
|
2699
|
+
if (!realAllowedRoot) return;
|
|
2700
|
+
const deleted = [];
|
|
2701
|
+
for (const transcriptPath of transcriptPaths) {
|
|
2702
|
+
const resolved = path5.resolve(transcriptPath);
|
|
2703
|
+
const relative4 = path5.relative(realAllowedRoot, resolved);
|
|
2704
|
+
if (relative4.startsWith("..") || path5.isAbsolute(relative4)) continue;
|
|
2705
|
+
await fsp7.rm(resolved, { force: true }).then(
|
|
2706
|
+
() => {
|
|
2707
|
+
deleted.push(resolved);
|
|
2708
|
+
},
|
|
2709
|
+
(err) => {
|
|
2710
|
+
console.warn(
|
|
2711
|
+
JSON.stringify({
|
|
2712
|
+
level: "warn",
|
|
2713
|
+
event: "session.rewind_subagent_delete_failed",
|
|
2714
|
+
sessionId: this.id,
|
|
2715
|
+
filePath: resolved,
|
|
2716
|
+
error: toErrorMessage(err),
|
|
2717
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2718
|
+
})
|
|
2719
|
+
);
|
|
2720
|
+
}
|
|
2721
|
+
);
|
|
2722
|
+
}
|
|
2723
|
+
if (deleted.length > 0) {
|
|
2724
|
+
this.events?.emit("session.rewind_subagents_deleted", {
|
|
2725
|
+
sessionId: this.id,
|
|
2726
|
+
transcriptPaths: deleted
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2479
2730
|
async clearSession() {
|
|
2480
2731
|
if (!this.filePath) return;
|
|
2481
2732
|
this.buffer.cancelTimer();
|
|
@@ -2588,7 +2839,8 @@ async function deleteSessionArtifacts({
|
|
|
2588
2839
|
fsp8.unlink(sessionPath(rootDir, id, ".summary.json")),
|
|
2589
2840
|
fsp8.unlink(sessionPath(rootDir, id, ".plan.json")),
|
|
2590
2841
|
fsp8.unlink(sessionPath(rootDir, id, ".tasks.json")),
|
|
2591
|
-
fsp8.unlink(sessionPath(rootDir, id, ".todos.json"))
|
|
2842
|
+
fsp8.unlink(sessionPath(rootDir, id, ".todos.json")),
|
|
2843
|
+
fsp8.unlink(shardManifestPath(rootDir, path7.dirname(id) === "." ? "" : path7.dirname(id)))
|
|
2592
2844
|
];
|
|
2593
2845
|
const results = await Promise.allSettled(deletions);
|
|
2594
2846
|
for (const result of results) {
|
|
@@ -2708,6 +2960,12 @@ function inheritsIntoFork(event) {
|
|
|
2708
2960
|
case "in_flight_start":
|
|
2709
2961
|
case "in_flight_end":
|
|
2710
2962
|
case "rewound":
|
|
2963
|
+
case "agent_spawned":
|
|
2964
|
+
case "agent_session_linked":
|
|
2965
|
+
case "agent_stopped":
|
|
2966
|
+
case "agent_error":
|
|
2967
|
+
case "delegate_started":
|
|
2968
|
+
case "delegate_completed":
|
|
2711
2969
|
return false;
|
|
2712
2970
|
// Parent snapshots describe mutations owned by the parent journal. A
|
|
2713
2971
|
// child that shares the current workspace must not inherit authority to
|
|
@@ -2786,6 +3044,25 @@ async function forkSession(host, id, opts = {}) {
|
|
|
2786
3044
|
// src/storage/session-store/load-cache.ts
|
|
2787
3045
|
var LOAD_CACHE_MAX_ENTRIES = 50;
|
|
2788
3046
|
var LOAD_CACHE_MAX_BYTES = 64 * 1024 * 1024;
|
|
3047
|
+
var SIZE_WALK_MAX_DEPTH = 12;
|
|
3048
|
+
function approximateRetainedBytes(value, depth = 0) {
|
|
3049
|
+
if (value === null || value === void 0) return 0;
|
|
3050
|
+
const type = typeof value;
|
|
3051
|
+
if (type === "string") return value.length * 2;
|
|
3052
|
+
if (type === "number" || type === "boolean") return 8;
|
|
3053
|
+
if (type !== "object") return 0;
|
|
3054
|
+
if (depth >= SIZE_WALK_MAX_DEPTH) return 0;
|
|
3055
|
+
if (Array.isArray(value)) {
|
|
3056
|
+
let bytes2 = 32;
|
|
3057
|
+
for (const item of value) bytes2 += approximateRetainedBytes(item, depth + 1);
|
|
3058
|
+
return bytes2;
|
|
3059
|
+
}
|
|
3060
|
+
let bytes = 48;
|
|
3061
|
+
for (const [key, child] of Object.entries(value)) {
|
|
3062
|
+
bytes += key.length * 2 + approximateRetainedBytes(child, depth + 1);
|
|
3063
|
+
}
|
|
3064
|
+
return bytes;
|
|
3065
|
+
}
|
|
2789
3066
|
var SessionLoadCache = class {
|
|
2790
3067
|
constructor(entries = /* @__PURE__ */ new Map()) {
|
|
2791
3068
|
this.entries = entries;
|
|
@@ -2826,25 +3103,27 @@ var SessionLoadCache = class {
|
|
|
2826
3103
|
}
|
|
2827
3104
|
set(id, stat11, data) {
|
|
2828
3105
|
this.delete(id);
|
|
2829
|
-
|
|
3106
|
+
const cost = approximateRetainedBytes(data);
|
|
3107
|
+
while (cost <= LOAD_CACHE_MAX_BYTES && (this.entries.size >= LOAD_CACHE_MAX_ENTRIES || this.bytes + cost > LOAD_CACHE_MAX_BYTES)) {
|
|
2830
3108
|
const oldest = this.entries.keys().next().value;
|
|
2831
3109
|
if (oldest === void 0) break;
|
|
2832
3110
|
this.delete(oldest);
|
|
2833
3111
|
}
|
|
2834
|
-
if (
|
|
2835
|
-
this.entries.set(id, { mtimeMs: stat11.mtimeMs, size: stat11.size, data });
|
|
2836
|
-
this.bytes +=
|
|
3112
|
+
if (cost <= LOAD_CACHE_MAX_BYTES) {
|
|
3113
|
+
this.entries.set(id, { mtimeMs: stat11.mtimeMs, size: stat11.size, data, bytes: cost });
|
|
3114
|
+
this.bytes += cost;
|
|
2837
3115
|
}
|
|
2838
3116
|
}
|
|
2839
3117
|
delete(sessionId) {
|
|
2840
3118
|
const cached = this.entries.get(sessionId);
|
|
2841
|
-
if (cached) this.bytes = Math.max(0, this.bytes - cached.size);
|
|
3119
|
+
if (cached) this.bytes = Math.max(0, this.bytes - (cached.bytes ?? cached.size));
|
|
2842
3120
|
this.entries.delete(sessionId);
|
|
2843
3121
|
}
|
|
2844
3122
|
};
|
|
2845
3123
|
|
|
2846
3124
|
// src/storage/session-store/load-session-data.ts
|
|
2847
3125
|
import { createReadStream as createReadStream3 } from "node:fs";
|
|
3126
|
+
import { stat as fspStat } from "node:fs/promises";
|
|
2848
3127
|
import { createInterface as createInterface3 } from "node:readline";
|
|
2849
3128
|
|
|
2850
3129
|
// src/storage/session-tool-call-ends.ts
|
|
@@ -2869,6 +3148,7 @@ function extractToolCallEnds(events) {
|
|
|
2869
3148
|
var DEFAULT_MAX_RETAINED_EVENT_BYTES = 96 * 1024 * 1024;
|
|
2870
3149
|
var EVICT_TO_FRACTION = 0.9;
|
|
2871
3150
|
var SNAPSHOT_ENVELOPE_BYTES = 256;
|
|
3151
|
+
var LOAD_PROGRESS_INTERVAL_MS = 250;
|
|
2872
3152
|
async function loadSessionDataFromFile(params) {
|
|
2873
3153
|
const events = [];
|
|
2874
3154
|
const eventBytes = [];
|
|
@@ -2886,12 +3166,24 @@ async function loadSessionDataFromFile(params) {
|
|
|
2886
3166
|
const messages = params.full ? [] : void 0;
|
|
2887
3167
|
const openToolUses = params.full ? /* @__PURE__ */ new Set() : void 0;
|
|
2888
3168
|
let exactJournalActive = false;
|
|
3169
|
+
let messageIndexOffset = 0;
|
|
2889
3170
|
let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
2890
3171
|
let lastSnapshot;
|
|
3172
|
+
const totalBytes = params.onLoadProgress ? (await fspStat(params.file)).size : 0;
|
|
2891
3173
|
const stream = createReadStream3(params.file, { encoding: "utf8" });
|
|
2892
3174
|
const rl = createInterface3({ input: stream, crlfDelay: Infinity });
|
|
3175
|
+
let loadedBytes = 0;
|
|
3176
|
+
let lastProgressAt = 0;
|
|
3177
|
+
const progressIntervalMs = params.progressIntervalMs ?? LOAD_PROGRESS_INTERVAL_MS;
|
|
2893
3178
|
try {
|
|
2894
3179
|
for await (const line of rl) {
|
|
3180
|
+
if (params.onLoadProgress) {
|
|
3181
|
+
loadedBytes += Buffer.byteLength(line, "utf8") + 1;
|
|
3182
|
+
if (Date.now() - lastProgressAt >= progressIntervalMs) {
|
|
3183
|
+
lastProgressAt = Date.now();
|
|
3184
|
+
params.onLoadProgress({ loadedBytes, totalBytes });
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
2895
3187
|
if (!line.trim()) continue;
|
|
2896
3188
|
try {
|
|
2897
3189
|
const parsed = JSON.parse(line);
|
|
@@ -2945,9 +3237,11 @@ async function loadSessionDataFromFile(params) {
|
|
|
2945
3237
|
messages,
|
|
2946
3238
|
openToolUses,
|
|
2947
3239
|
exactJournalActive,
|
|
3240
|
+
messageIndexOffset,
|
|
2948
3241
|
usage
|
|
2949
3242
|
});
|
|
2950
3243
|
exactJournalActive = replayState.exactJournalActive;
|
|
3244
|
+
messageIndexOffset = replayState.messageIndexOffset;
|
|
2951
3245
|
usage = replayState.usage ?? usage;
|
|
2952
3246
|
} else if (ev.type === "llm_response") {
|
|
2953
3247
|
usage = accumulateUsage(usage, ev);
|
|
@@ -2955,6 +3249,9 @@ async function loadSessionDataFromFile(params) {
|
|
|
2955
3249
|
} catch {
|
|
2956
3250
|
}
|
|
2957
3251
|
}
|
|
3252
|
+
if (params.onLoadProgress) {
|
|
3253
|
+
params.onLoadProgress({ loadedBytes: totalBytes, totalBytes });
|
|
3254
|
+
}
|
|
2958
3255
|
} finally {
|
|
2959
3256
|
rl.close();
|
|
2960
3257
|
stream.close();
|
|
@@ -2967,7 +3264,7 @@ async function loadSessionDataFromFile(params) {
|
|
|
2967
3264
|
detail: `${openToolUses.size} tool_use blocks without matching results - replay repaired`
|
|
2968
3265
|
});
|
|
2969
3266
|
}
|
|
2970
|
-
const repaired = repairToolUseAdjacency(messages);
|
|
3267
|
+
const repaired = repairToolUseAdjacency(messages, { preserveTrailingToolUse: true });
|
|
2971
3268
|
if (repaired.report.changed) {
|
|
2972
3269
|
params.events?.emit("session.damaged", {
|
|
2973
3270
|
sessionId: params.id,
|
|
@@ -3020,14 +3317,16 @@ function isSessionEventLike(value) {
|
|
|
3020
3317
|
function replaySessionEvent(params) {
|
|
3021
3318
|
const { ev, messages, openToolUses } = params;
|
|
3022
3319
|
let exactJournalActive = params.exactJournalActive;
|
|
3320
|
+
let messageIndexOffset = params.messageIndexOffset;
|
|
3023
3321
|
if (ev.type === "message_appended" && ev.version === 1) {
|
|
3024
3322
|
const message = replayableMessage(ev.message, ev.ts);
|
|
3025
3323
|
if (message) {
|
|
3026
|
-
if (!exactJournalActive) {
|
|
3324
|
+
if (!exactJournalActive && shouldReplaceLegacyReplay(messages, ev.ts)) {
|
|
3027
3325
|
messages.length = 0;
|
|
3028
3326
|
openToolUses.clear();
|
|
3029
|
-
|
|
3327
|
+
messageIndexOffset = 0;
|
|
3030
3328
|
}
|
|
3329
|
+
exactJournalActive = true;
|
|
3031
3330
|
messages.push(message);
|
|
3032
3331
|
trackMessageToolState(message, openToolUses);
|
|
3033
3332
|
} else {
|
|
@@ -3035,8 +3334,9 @@ function replaySessionEvent(params) {
|
|
|
3035
3334
|
}
|
|
3036
3335
|
} else if (ev.type === "message_updated" && ev.version === 1) {
|
|
3037
3336
|
const message = replayableMessage(ev.message, ev.ts);
|
|
3038
|
-
|
|
3039
|
-
|
|
3337
|
+
const index = ev.index - messageIndexOffset;
|
|
3338
|
+
if (message && exactJournalActive && index >= 0 && index < messages.length) {
|
|
3339
|
+
messages[index] = message;
|
|
3040
3340
|
openToolUses.clear();
|
|
3041
3341
|
for (const current of messages) trackMessageToolState(current, openToolUses);
|
|
3042
3342
|
} else {
|
|
@@ -3050,12 +3350,15 @@ function replaySessionEvent(params) {
|
|
|
3050
3350
|
);
|
|
3051
3351
|
} else if (applyContextSnapshot(messages, openToolUses, ev.messages)) {
|
|
3052
3352
|
exactJournalActive = true;
|
|
3353
|
+
messageIndexOffset = 0;
|
|
3053
3354
|
} else {
|
|
3054
3355
|
emitDamaged(params, "Ignored malformed messages_replaced event");
|
|
3055
3356
|
}
|
|
3056
3357
|
} else if (ev.type === "messages_dropped" && ev.version === 1) {
|
|
3057
3358
|
if (exactJournalActive && Number.isInteger(ev.count) && ev.count > 0) {
|
|
3058
|
-
|
|
3359
|
+
const dropped = Math.min(ev.count, messages.length);
|
|
3360
|
+
messages.splice(0, dropped);
|
|
3361
|
+
messageIndexOffset += dropped;
|
|
3059
3362
|
openToolUses.clear();
|
|
3060
3363
|
for (const current of messages) trackMessageToolState(current, openToolUses);
|
|
3061
3364
|
} else if (!exactJournalActive) {
|
|
@@ -3069,7 +3372,9 @@ function replaySessionEvent(params) {
|
|
|
3069
3372
|
params,
|
|
3070
3373
|
`Ignored context_snapshot event whose payload was stripped before persistence (${String(ev.messagesOmitted)} messages)`
|
|
3071
3374
|
);
|
|
3072
|
-
} else if (
|
|
3375
|
+
} else if (applyContextSnapshot(messages, openToolUses, ev.messages)) {
|
|
3376
|
+
messageIndexOffset = 0;
|
|
3377
|
+
} else {
|
|
3073
3378
|
emitDamaged(params, "Ignored malformed context_snapshot event");
|
|
3074
3379
|
}
|
|
3075
3380
|
} else if (ev.type === "messages_replaced" || ev.type === "message_appended" || ev.type === "message_updated" || ev.type === "messages_dropped") {
|
|
@@ -3084,11 +3389,11 @@ function replaySessionEvent(params) {
|
|
|
3084
3389
|
if (b.type === "tool_use") openToolUses.add(b.id);
|
|
3085
3390
|
}
|
|
3086
3391
|
}
|
|
3087
|
-
return { exactJournalActive, usage: accumulateUsage(params.usage, ev) };
|
|
3392
|
+
return { exactJournalActive, messageIndexOffset, usage: accumulateUsage(params.usage, ev) };
|
|
3088
3393
|
} else if (!exactJournalActive && ev.type === "tool_result") {
|
|
3089
3394
|
if (!openToolUses.has(ev.id)) {
|
|
3090
3395
|
emitDamaged(params, `Orphan tool_result "${ev.id}" has no matching tool_use`);
|
|
3091
|
-
return { exactJournalActive };
|
|
3396
|
+
return { exactJournalActive, messageIndexOffset };
|
|
3092
3397
|
}
|
|
3093
3398
|
openToolUses.delete(ev.id);
|
|
3094
3399
|
const resultBlock = {
|
|
@@ -3105,7 +3410,12 @@ function replaySessionEvent(params) {
|
|
|
3105
3410
|
messages.push({ role: "user", content: [resultBlock], ts: ev.ts });
|
|
3106
3411
|
}
|
|
3107
3412
|
}
|
|
3108
|
-
return { exactJournalActive };
|
|
3413
|
+
return { exactJournalActive, messageIndexOffset };
|
|
3414
|
+
}
|
|
3415
|
+
function shouldReplaceLegacyReplay(messages, eventTs) {
|
|
3416
|
+
if (messages.length === 0) return true;
|
|
3417
|
+
const lastTs = messages[messages.length - 1]?.ts;
|
|
3418
|
+
return typeof lastTs === "string" && lastTs >= eventTs;
|
|
3109
3419
|
}
|
|
3110
3420
|
function accumulateUsage(usage, ev) {
|
|
3111
3421
|
return {
|
|
@@ -3128,7 +3438,7 @@ import * as path8 from "node:path";
|
|
|
3128
3438
|
function isPrunableSessionJsonl(name) {
|
|
3129
3439
|
return isSessionTranscriptFileName(name);
|
|
3130
3440
|
}
|
|
3131
|
-
async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession) {
|
|
3441
|
+
async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession, isSessionInUse) {
|
|
3132
3442
|
const cutoff = Date.now() - maxAgeDays * 864e5;
|
|
3133
3443
|
let deleted = 0;
|
|
3134
3444
|
const pruneFile = async (dir, name, prefix) => {
|
|
@@ -3141,6 +3451,7 @@ async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession) {
|
|
|
3141
3451
|
}
|
|
3142
3452
|
const base = name.replace(/\.jsonl$/, "");
|
|
3143
3453
|
const id = prefix ? `${prefix}/${base}` : base;
|
|
3454
|
+
if (isSessionInUse && await isSessionInUse(id)) return;
|
|
3144
3455
|
await deleteSession(id);
|
|
3145
3456
|
deleted++;
|
|
3146
3457
|
};
|
|
@@ -3246,8 +3557,8 @@ var MAX_REVALIDATE_BYTES = 5 * 1024 * 1024;
|
|
|
3246
3557
|
var VALIDATION_CONCURRENCY = 8;
|
|
3247
3558
|
var NOTICE_PATH_LIMIT = 20;
|
|
3248
3559
|
function isInside2(root, target) {
|
|
3249
|
-
const
|
|
3250
|
-
return
|
|
3560
|
+
const relative4 = path9.relative(root, target);
|
|
3561
|
+
return relative4 === "" || !relative4.startsWith("..") && !path9.isAbsolute(relative4);
|
|
3251
3562
|
}
|
|
3252
3563
|
function errno(err) {
|
|
3253
3564
|
return err && typeof err === "object" && "code" in err ? String(err.code) : void 0;
|
|
@@ -3357,8 +3668,8 @@ function formatResumeValidationNotice(validation, projectRoot) {
|
|
|
3357
3668
|
if (validation.staleFiles.length === 0) return null;
|
|
3358
3669
|
const root = path9.resolve(projectRoot);
|
|
3359
3670
|
const shown = validation.staleFiles.slice(0, NOTICE_PATH_LIMIT).map((entry) => {
|
|
3360
|
-
const
|
|
3361
|
-
const display = isInside2(root, entry.path) ?
|
|
3671
|
+
const relative4 = path9.relative(root, entry.path);
|
|
3672
|
+
const display = isInside2(root, entry.path) ? relative4 || "." : entry.path;
|
|
3362
3673
|
return `- ${JSON.stringify(display)} [${entry.status}]`;
|
|
3363
3674
|
});
|
|
3364
3675
|
const omitted = validation.staleFiles.length - shown.length;
|
|
@@ -3420,7 +3731,6 @@ async function summarizeSessionEventSequence(opts) {
|
|
|
3420
3731
|
}
|
|
3421
3732
|
}
|
|
3422
3733
|
const toolBreakdown = {};
|
|
3423
|
-
let outcome;
|
|
3424
3734
|
let lastEventType;
|
|
3425
3735
|
let hasError = false;
|
|
3426
3736
|
let sawStart = false;
|
|
@@ -3467,23 +3777,21 @@ async function summarizeSessionEventSequence(opts) {
|
|
|
3467
3777
|
}
|
|
3468
3778
|
} else if (e.type === "llm_response") {
|
|
3469
3779
|
messageCount++;
|
|
3470
|
-
|
|
3471
|
-
|
|
3780
|
+
if (e.usage) {
|
|
3781
|
+
tokenIn += effectiveInputTokens(e.usage);
|
|
3782
|
+
tokenOut += e.usage.output ?? 0;
|
|
3783
|
+
}
|
|
3784
|
+
if (e.model) model = e.model;
|
|
3785
|
+
if (e.provider) provider = e.provider;
|
|
3472
3786
|
} else if (e.type === "in_flight_start") iterationCount++;
|
|
3473
3787
|
else if (e.type === "tool_call_start") {
|
|
3474
3788
|
toolCallCount++;
|
|
3475
3789
|
toolBreakdown[e.name] = (toolBreakdown[e.name] ?? 0) + 1;
|
|
3476
3790
|
} else if (e.type === "tool_result" && e.isError) toolErrorCount++;
|
|
3477
3791
|
else if (e.type === "file_snapshot") fileChangeCount += e.files.length;
|
|
3478
|
-
else if (e
|
|
3479
|
-
}
|
|
3480
|
-
if (lastEventType === "session_end") {
|
|
3481
|
-
outcome = "completed";
|
|
3482
|
-
} else if (lastEventType === "in_flight_start") {
|
|
3483
|
-
outcome = "aborted";
|
|
3484
|
-
} else if (hasError) {
|
|
3485
|
-
outcome = "error";
|
|
3792
|
+
else if (isSessionErrorEvent(e)) hasError = true;
|
|
3486
3793
|
}
|
|
3794
|
+
const outcome = resolveSessionOutcome(lastEventType, hasError);
|
|
3487
3795
|
return {
|
|
3488
3796
|
id,
|
|
3489
3797
|
title,
|
|
@@ -3594,10 +3902,7 @@ async function executeResumeSession(params) {
|
|
|
3594
3902
|
let resumeValidation;
|
|
3595
3903
|
if (projectRoot) {
|
|
3596
3904
|
try {
|
|
3597
|
-
const validationEvents = eventsDropped > 0 ? (await searchEvents(
|
|
3598
|
-
canonicalId,
|
|
3599
|
-
(ev) => ev.type === "file_observation"
|
|
3600
|
-
)).map((h) => h.event) : data.events;
|
|
3905
|
+
const validationEvents = eventsDropped > 0 ? (await searchEvents(canonicalId, (ev) => ev.type === "file_observation")).map((h) => h.event) : data.events;
|
|
3601
3906
|
resumeValidation = await validateResumeFileObservations(validationEvents, projectRoot);
|
|
3602
3907
|
const notice = formatResumeValidationNotice(resumeValidation, projectRoot);
|
|
3603
3908
|
if (notice) {
|
|
@@ -3635,26 +3940,32 @@ async function executeResumeSession(params) {
|
|
|
3635
3940
|
});
|
|
3636
3941
|
}
|
|
3637
3942
|
const carriedMessages = data.messages.filter((message) => !isResumeNoticeMessage(message));
|
|
3943
|
+
const recoveredMessages = appendSyntheticRecoveryMessages(
|
|
3944
|
+
carriedMessages,
|
|
3945
|
+
recoveryPlan.pendingEvents,
|
|
3946
|
+
synthesizedResults
|
|
3947
|
+
);
|
|
3948
|
+
const { pendingToolUseCount: _healedPendingToolUseCount, ...dataWithoutPendingToolUseCount } = data;
|
|
3949
|
+
const resumedBase = synthesizedResults.length > 0 ? dataWithoutPendingToolUseCount : data;
|
|
3638
3950
|
const resumedData = {
|
|
3639
|
-
...
|
|
3951
|
+
...resumedBase,
|
|
3640
3952
|
...resumeValidation ? { resumeValidation } : {},
|
|
3641
|
-
messages: [...
|
|
3953
|
+
messages: [...recoveredMessages, ...noticeMessages]
|
|
3642
3954
|
};
|
|
3643
3955
|
let handle;
|
|
3644
3956
|
try {
|
|
3645
3957
|
handle = await openSessionForAppend(file);
|
|
3646
3958
|
} catch (err) {
|
|
3647
3959
|
emitSessionStoreError(events, canonicalId, file, "resume", toErrorMessage(err), false);
|
|
3648
|
-
throw new Error(
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
);
|
|
3960
|
+
throw new Error(`Failed to open session "${canonicalId}" for append: ${toErrorMessage(err)}`, {
|
|
3961
|
+
cause: err
|
|
3962
|
+
});
|
|
3652
3963
|
}
|
|
3653
3964
|
try {
|
|
3654
3965
|
const writer = new FileSessionWriter(
|
|
3655
3966
|
canonicalId,
|
|
3656
3967
|
handle,
|
|
3657
|
-
|
|
3968
|
+
data.metadata.startedAt,
|
|
3658
3969
|
{
|
|
3659
3970
|
id: canonicalId,
|
|
3660
3971
|
model: data.metadata.model,
|
|
@@ -3703,6 +4014,71 @@ async function executeResumeSession(params) {
|
|
|
3703
4014
|
throw err;
|
|
3704
4015
|
}
|
|
3705
4016
|
}
|
|
4017
|
+
function appendSyntheticRecoveryMessages(messages, pendingEvents, synthesizedResults) {
|
|
4018
|
+
if (synthesizedResults.length === 0) return messages;
|
|
4019
|
+
const next = [...messages];
|
|
4020
|
+
for (const result of synthesizedResults) {
|
|
4021
|
+
if (result.type !== "tool_result") continue;
|
|
4022
|
+
const toolUse = findToolUseBlock(next, result.id) ?? findToolUseBlockInEvents(pendingEvents, result.id);
|
|
4023
|
+
if (!toolUse) continue;
|
|
4024
|
+
if (!isLastMessageToolUseFor(next, result.id)) {
|
|
4025
|
+
next.push({
|
|
4026
|
+
role: "assistant",
|
|
4027
|
+
content: [toolUse],
|
|
4028
|
+
ts: result.ts
|
|
4029
|
+
});
|
|
4030
|
+
}
|
|
4031
|
+
next.push({
|
|
4032
|
+
role: "user",
|
|
4033
|
+
content: [
|
|
4034
|
+
{
|
|
4035
|
+
type: "tool_result",
|
|
4036
|
+
tool_use_id: result.id,
|
|
4037
|
+
content: typeof result.content === "string" ? result.content : JSON.stringify(result.content),
|
|
4038
|
+
is_error: result.isError
|
|
4039
|
+
}
|
|
4040
|
+
],
|
|
4041
|
+
ts: result.ts
|
|
4042
|
+
});
|
|
4043
|
+
}
|
|
4044
|
+
return next;
|
|
4045
|
+
}
|
|
4046
|
+
function findToolUseBlock(messages, id) {
|
|
4047
|
+
for (const message of messages) {
|
|
4048
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
4049
|
+
for (const block of message.content) {
|
|
4050
|
+
if (block.type === "tool_use" && block.id === id) return block;
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
return null;
|
|
4054
|
+
}
|
|
4055
|
+
function findToolUseBlockInEvents(events, id) {
|
|
4056
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
4057
|
+
const event = events[i];
|
|
4058
|
+
if (!event) continue;
|
|
4059
|
+
if (event.type === "llm_response") {
|
|
4060
|
+
const block = event.content.find((item) => item.type === "tool_use" && item.id === id);
|
|
4061
|
+
if (block) return block;
|
|
4062
|
+
} else if (event.type === "message_appended" && event.message.role === "assistant" && Array.isArray(event.message.content)) {
|
|
4063
|
+
const block = event.message.content.find(
|
|
4064
|
+
(item) => item.type === "tool_use" && item.id === id
|
|
4065
|
+
);
|
|
4066
|
+
if (block) return block;
|
|
4067
|
+
} else if (event.type === "tool_use" && event.id === id) {
|
|
4068
|
+
return { type: "tool_use", id: event.id, name: event.name, input: recordInput(event.input) };
|
|
4069
|
+
} else if (event.type === "tool_call_start" && event.id === id) {
|
|
4070
|
+
return { type: "tool_use", id: event.id, name: event.name, input: recordInput(event.input) };
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
return null;
|
|
4074
|
+
}
|
|
4075
|
+
function recordInput(input) {
|
|
4076
|
+
return input !== null && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
4077
|
+
}
|
|
4078
|
+
function isLastMessageToolUseFor(messages, id) {
|
|
4079
|
+
const last = messages[messages.length - 1];
|
|
4080
|
+
return last?.role === "assistant" && Array.isArray(last.content) && last.content.some((block) => block.type === "tool_use" && block.id === id);
|
|
4081
|
+
}
|
|
3706
4082
|
async function openSessionForAppend(file) {
|
|
3707
4083
|
const handle = await fsp12.open(file, "a+", 384);
|
|
3708
4084
|
try {
|
|
@@ -3723,6 +4099,7 @@ async function openSessionForAppend(file) {
|
|
|
3723
4099
|
|
|
3724
4100
|
// src/storage/session-store/search-events.ts
|
|
3725
4101
|
import * as fsp13 from "node:fs/promises";
|
|
4102
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3726
4103
|
async function searchSessionEvents(args) {
|
|
3727
4104
|
const { file, secretScrubber, predicate, limit, signal } = args;
|
|
3728
4105
|
const out = [];
|
|
@@ -3739,6 +4116,7 @@ async function searchSessionEvents(args) {
|
|
|
3739
4116
|
fh = await fsp13.open(file, "r");
|
|
3740
4117
|
const CHUNK = 64 * 1024;
|
|
3741
4118
|
const buf = Buffer.alloc(CHUNK);
|
|
4119
|
+
const decoder = new StringDecoder("utf8");
|
|
3742
4120
|
let leftover = "";
|
|
3743
4121
|
let eventIndex = 0;
|
|
3744
4122
|
for (let position = 0; ; position += buf.byteLength) {
|
|
@@ -3748,7 +4126,7 @@ async function searchSessionEvents(args) {
|
|
|
3748
4126
|
}
|
|
3749
4127
|
const { bytesRead } = await fh.read(buf, 0, CHUNK, position);
|
|
3750
4128
|
if (bytesRead === 0) break;
|
|
3751
|
-
const text = leftover + buf.subarray(0, bytesRead)
|
|
4129
|
+
const text = leftover + decoder.write(buf.subarray(0, bytesRead));
|
|
3752
4130
|
const parts = text.split("\n");
|
|
3753
4131
|
leftover = parts.pop() ?? "";
|
|
3754
4132
|
for (const line of parts) {
|
|
@@ -3762,6 +4140,7 @@ async function searchSessionEvents(args) {
|
|
|
3762
4140
|
eventIndex++;
|
|
3763
4141
|
}
|
|
3764
4142
|
}
|
|
4143
|
+
leftover += decoder.end();
|
|
3765
4144
|
const trailing = parseSessionEventLine(leftover.trim() ? leftover : "", secretScrubber);
|
|
3766
4145
|
if (trailing && predicate(trailing, eventIndex, trailing.ts)) {
|
|
3767
4146
|
out.push({ event: trailing, eventIndex, ts: trailing.ts });
|
|
@@ -4037,7 +4416,10 @@ async function compactIndexInner(indexFile, entries, deletedIds) {
|
|
|
4037
4416
|
parts.push(JSON.stringify({ action: "delete", id }));
|
|
4038
4417
|
}
|
|
4039
4418
|
}
|
|
4040
|
-
if (parts.length === 0)
|
|
4419
|
+
if (parts.length === 0) {
|
|
4420
|
+
await atomicWrite(indexFile, "", { mode: 384 });
|
|
4421
|
+
return;
|
|
4422
|
+
}
|
|
4041
4423
|
const lines = parts.join("\n") + "\n";
|
|
4042
4424
|
await atomicWrite(indexFile, lines, { mode: 384 });
|
|
4043
4425
|
}
|
|
@@ -4412,7 +4794,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
4412
4794
|
if (resolution.status === "resolved") return resolution.id;
|
|
4413
4795
|
throw sessionIdResolutionError(resolution);
|
|
4414
4796
|
}
|
|
4415
|
-
async resume(id) {
|
|
4797
|
+
async resume(id, onLoadProgress) {
|
|
4416
4798
|
const canonicalId = await this.resolveId(id);
|
|
4417
4799
|
const file = this.sessionPath(canonicalId, ".jsonl");
|
|
4418
4800
|
return executeResumeSession({
|
|
@@ -4425,7 +4807,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
4425
4807
|
checkpointCas: this.checkpointCas,
|
|
4426
4808
|
onAppend: this.onAppend,
|
|
4427
4809
|
onAppendBatch: this.onAppendBatch,
|
|
4428
|
-
load: (loadId) => this.load(loadId),
|
|
4810
|
+
load: (loadId) => this.load(loadId, onLoadProgress),
|
|
4429
4811
|
readSummaryManifest: (summaryId) => this.readSummaryManifest(summaryId),
|
|
4430
4812
|
searchEvents: (searchId, pred) => this.searchEvents(searchId, pred),
|
|
4431
4813
|
persistCatalogSummary: (sum) => this.persistCatalogSummary(sum),
|
|
@@ -4433,13 +4815,13 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
4433
4815
|
sessionsDir: this.dir
|
|
4434
4816
|
});
|
|
4435
4817
|
}
|
|
4436
|
-
async load(id) {
|
|
4437
|
-
return this.loadInternal(id, { full: true });
|
|
4818
|
+
async load(id, onLoadProgress) {
|
|
4819
|
+
return this.loadInternal(id, { full: true }, onLoadProgress);
|
|
4438
4820
|
}
|
|
4439
4821
|
async loadEventsOnly(id) {
|
|
4440
4822
|
return this.loadInternal(id, { full: false });
|
|
4441
4823
|
}
|
|
4442
|
-
async loadInternal(id, mode) {
|
|
4824
|
+
async loadInternal(id, mode, onLoadProgress) {
|
|
4443
4825
|
const file = this.sessionPath(id, ".jsonl");
|
|
4444
4826
|
const t0 = Date.now();
|
|
4445
4827
|
let outcome = "success";
|
|
@@ -4451,6 +4833,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
4451
4833
|
const cached = this.loadCache.getFresh(id, stat11, mode.full);
|
|
4452
4834
|
if (cached) {
|
|
4453
4835
|
cacheHit = true;
|
|
4836
|
+
onLoadProgress?.({ loadedBytes: stat11.size, totalBytes: stat11.size });
|
|
4454
4837
|
return cached;
|
|
4455
4838
|
}
|
|
4456
4839
|
const data = await loadSessionDataFromFile({
|
|
@@ -4458,7 +4841,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
4458
4841
|
file,
|
|
4459
4842
|
full: mode.full,
|
|
4460
4843
|
events: this.events,
|
|
4461
|
-
secretScrubber: this.secretScrubber
|
|
4844
|
+
secretScrubber: this.secretScrubber,
|
|
4845
|
+
onLoadProgress
|
|
4462
4846
|
});
|
|
4463
4847
|
if (mode.full) {
|
|
4464
4848
|
this.loadCache.set(id, stat11, data);
|
|
@@ -4910,7 +5294,12 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
4910
5294
|
holderId: this.maintenanceHolderId
|
|
4911
5295
|
});
|
|
4912
5296
|
}
|
|
4913
|
-
const deleted = await pruneSessionFiles(
|
|
5297
|
+
const deleted = await pruneSessionFiles(
|
|
5298
|
+
this.dir,
|
|
5299
|
+
maxAgeDays,
|
|
5300
|
+
(id) => this.deleteSession(id),
|
|
5301
|
+
this.isSessionInUse
|
|
5302
|
+
);
|
|
4914
5303
|
if (deleted > 0) {
|
|
4915
5304
|
await this.compactIndex().catch(() => void 0);
|
|
4916
5305
|
}
|
|
@@ -4926,6 +5315,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
4926
5315
|
ensureShardDir: (sid) => this.ensureShardDir(sid),
|
|
4927
5316
|
sessionPath: (sid, ext) => this.sessionPath(sid, ext)
|
|
4928
5317
|
});
|
|
5318
|
+
if (!this.catalogClient) {
|
|
5319
|
+
await this.appendToIndexStrict(await this.summaryFor(canonical));
|
|
5320
|
+
}
|
|
4929
5321
|
this.clearLoadCache(canonical);
|
|
4930
5322
|
if (id !== canonical) this.clearLoadCache(id);
|
|
4931
5323
|
}
|
|
@@ -4954,6 +5346,7 @@ export {
|
|
|
4954
5346
|
scrubPersistedSessionSummary,
|
|
4955
5347
|
extractInterruptedTools,
|
|
4956
5348
|
SessionRecovery,
|
|
5349
|
+
summarizeSessionFile,
|
|
4957
5350
|
DefaultSessionStore
|
|
4958
5351
|
};
|
|
4959
|
-
//# sourceMappingURL=chunk-
|
|
5352
|
+
//# sourceMappingURL=chunk-NSOFPKGT.js.map
|