@sema-agent/server 7.43.0 → 7.44.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/USAGE.md +18 -0
- package/dist/adoption/plan.js +31 -192
- package/dist/adoption/quiesce.js +3 -86
- package/dist/adoption/runner.js +3 -140
- package/dist/adoption/sql.js +0 -74
- package/dist/adoption/wire.js +0 -80
- package/dist/approval-ask-machine.js +0 -75
- package/dist/approval-card.js +0 -323
- package/dist/approval-deny-reasons.js +0 -42
- package/dist/approval-hmac.js +0 -35
- package/dist/approval-reconciler.js +8 -166
- package/dist/approval.js +4 -67
- package/dist/audit.js +1 -44
- package/dist/auth-bridge.js +4 -47
- package/dist/auth-keys.js +0 -23
- package/dist/bake-runner/main.js +4 -65
- package/dist/bake-runner/protocol.js +3 -58
- package/dist/bake-runner/runner.js +5 -91
- package/dist/bench/l8/artifact.js +2 -61
- package/dist/bench/l8/escape.js +0 -25
- package/dist/bench/l8/index.js +0 -14
- package/dist/bench/l8/probes.js +0 -60
- package/dist/bench/l8/run-probes.js +2 -45
- package/dist/bench/s1/arms.js +14 -189
- package/dist/bench/s1/live-deps.js +11 -253
- package/dist/bench/s1/oracle.js +1 -36
- package/dist/bench/s1/repair-oracle-adapter.js +0 -30
- package/dist/bench/s1/reviewer.js +0 -34
- package/dist/bench/s1/row.js +0 -62
- package/dist/bench/s1/run-firm.js +4 -70
- package/dist/bench/s1/runner-ctx.js +0 -40
- package/dist/bench/s1/tasks.js +0 -115
- package/dist/boot/adoption.js +0 -20
- package/dist/boot/budget-tracing.js +3 -55
- package/dist/boot/config-center.js +43 -551
- package/dist/boot/coordinators.js +1 -69
- package/dist/boot/crash-last.js +0 -18
- package/dist/boot/deferred-sandbox-path-env.js +3 -119
- package/dist/boot/execution-env.js +5 -171
- package/dist/boot/governance-seams.js +0 -92
- package/dist/boot/leader.js +0 -69
- package/dist/boot/limit-sync.js +0 -2
- package/dist/boot/memory-boundary.js +3 -91
- package/dist/boot/org-memory.js +1 -25
- package/dist/boot/parked-revive-gate.js +0 -150
- package/dist/boot/permission-rules-audit.js +1 -56
- package/dist/boot/reapers.js +15 -273
- package/dist/boot/resolve-spec.js +9 -768
- package/dist/boot/retention-lane.js +4 -87
- package/dist/boot/runner-deps.js +3 -297
- package/dist/boot/runtime-caps.js +0 -40
- package/dist/boot/session-faces.js +2 -133
- package/dist/boot/shutdown.js +9 -83
- package/dist/boot/side-query-lane.js +2 -137
- package/dist/boot/stores.js +15 -215
- package/dist/boot/task-list-lane.js +0 -18
- package/dist/boot/webfetch-summarize-lane.js +0 -47
- package/dist/boot/workflow-orchestration.js +3 -99
- package/dist/boot-reclaim.js +0 -20
- package/dist/bounded-session-map.js +0 -19
- package/dist/brain.js +2 -139
- package/dist/budget.js +9 -244
- package/dist/capabilities/builtin-tools.js +0 -3
- package/dist/capabilities/center-plugins.js +6 -51
- package/dist/capabilities/center-prompts.js +6 -71
- package/dist/capabilities/code-review-council.js +7 -40
- package/dist/capabilities/collab-workflows.js +1 -44
- package/dist/capabilities/hands-lane.js +0 -65
- package/dist/capabilities/memory-notice.js +0 -70
- package/dist/capabilities/prompt.js +0 -14
- package/dist/capabilities/prompts/code-review.js +0 -14
- package/dist/capabilities/prompts/identity.js +0 -6
- package/dist/capabilities/prompts/team.js +0 -4
- package/dist/capabilities/repo-tools.js +0 -34
- package/dist/capabilities/sandbox-file-send.js +3 -92
- package/dist/capabilities/scenarios.d.ts +0 -1
- package/dist/capabilities/scenarios.js +18 -279
- package/dist/capabilities/select-environment-tool.js +0 -32
- package/dist/capabilities/send-user-file-tool.js +2 -66
- package/dist/capabilities/skills.d.ts +1 -2
- package/dist/capabilities/skills.js +3 -21
- package/dist/capabilities/team.d.ts +3 -11
- package/dist/capabilities/team.js +3 -51
- package/dist/capabilities/tool-defer.js +0 -3
- package/dist/config-center/apply-effective.js +15 -330
- package/dist/config-center/apply-ledger.js +5 -38
- package/dist/config-center/facade.js +0 -41
- package/dist/config-center/hot-keys-registry.js +0 -20
- package/dist/config-center/http-client.js +2 -125
- package/dist/config-center/mcp-revocation.js +2 -34
- package/dist/config-center/read-face.js +0 -59
- package/dist/config-center/restart-signal.js +1 -85
- package/dist/config-center/skills-mcp.d.ts +1 -1
- package/dist/config-center/skills-mcp.js +9 -76
- package/dist/config-center/stage-limits.js +8 -35
- package/dist/config-invariants.js +0 -16
- package/dist/config-lkg.js +0 -42
- package/dist/config-provider.js +3 -186
- package/dist/config-types.js +0 -5
- package/dist/config.js +107 -1145
- package/dist/degenerate-instrument.js +3 -67
- package/dist/deployment-governance.js +0 -124
- package/dist/digest-form.js +0 -12
- package/dist/elicitation.js +3 -86
- package/dist/env-facts.js +7 -75
- package/dist/fleet/fleet-bus.js +34 -507
- package/dist/fleet/fleet-reconciler.js +9 -149
- package/dist/fleet/fleet-terminal-window.js +10 -178
- package/dist/fleet/subagent-tail-bus.js +3 -72
- package/dist/fleet-client.js +10 -70
- package/dist/fleet-lease.js +5 -79
- package/dist/git-api-kind.js +0 -3
- package/dist/governance-ask-marks.js +2 -78
- package/dist/hooks/branch-transcript.js +0 -74
- package/dist/hooks/cc-agent-hook-prompt.js +0 -29
- package/dist/hooks/cc-stop-prompt.js +1 -46
- package/dist/hooks/hook-llm.js +1 -53
- package/dist/hooks/hook-runner.js +20 -414
- package/dist/http/active-run-conflict.js +4 -129
- package/dist/http/cursor-fingerprint.d.ts +5 -0
- package/dist/http/cursor-fingerprint.js +5 -0
- package/dist/http/idempotency.js +0 -37
- package/dist/http/principal-gate.js +3 -40
- package/dist/http/route-ctx.js +0 -9
- package/dist/http/routes/a2a-serve.js +7 -319
- package/dist/http/routes/admin-config-refresh.js +0 -4
- package/dist/http/routes/admin-drain.js +0 -10
- package/dist/http/routes/adoption.js +1 -29
- package/dist/http/routes/agents-roster.js +1 -47
- package/dist/http/routes/approvals-assistant.js +32 -395
- package/dist/http/routes/attachments.js +4 -22
- package/dist/http/routes/capabilities.js +3 -420
- package/dist/http/routes/diagnostics.js +1 -81
- package/dist/http/routes/fleet.js +9 -185
- package/dist/http/routes/images.js +14 -239
- package/dist/http/routes/leader.js +0 -13
- package/dist/http/routes/memory-bundle.js +1 -61
- package/dist/http/routes/memory-policy.js +9 -99
- package/dist/http/routes/notify-wake.js +3 -37
- package/dist/http/routes/observability.js +2 -19
- package/dist/http/routes/retention-ops.js +2 -34
- package/dist/http/routes/rules.js +1 -93
- package/dist/http/routes/runs.js +64 -873
- package/dist/http/routes/session-sync.js +19 -258
- package/dist/http/routes/sessions-list.js +17 -43
- package/dist/http/routes/sessions.js +34 -226
- package/dist/http/routes/shared-memory.js +5 -36
- package/dist/http/routes/side-query.js +1 -87
- package/dist/http/routes/tasks.js +52 -722
- package/dist/http/routes/trace-usage.js +32 -195
- package/dist/http/routes/workflows.js +19 -195
- package/dist/http/run-meta.js +0 -6
- package/dist/http/send.js +0 -32
- package/dist/http/server.js +92 -1597
- package/dist/http/sse-lifecycle.js +2 -13
- package/dist/http/sse-log.js +3 -48
- package/dist/http/tar.js +5 -21
- package/dist/http/verify-rounds.js +0 -5
- package/dist/http/wire-gate.js +0 -9
- package/dist/http/workspace-content.js +0 -10
- package/dist/images/bake-validate.js +1 -70
- package/dist/images/manifest.js +1 -6
- package/dist/index.js +0 -21
- package/dist/key-resolver.js +2 -17
- package/dist/leader/diffout.js +1 -20
- package/dist/leader/diffup.js +0 -47
- package/dist/leader/endpoint.js +2 -57
- package/dist/leader/fanout.js +3 -45
- package/dist/leader/grader-env-factory.js +3 -72
- package/dist/leader/leader.js +5 -156
- package/dist/leader/merge.js +8 -115
- package/dist/leader/planner.js +3 -54
- package/dist/leader/repair-oracle.js +1 -60
- package/dist/leader/repair-wire.js +2 -79
- package/dist/leader/wire.js +8 -307
- package/dist/lsp/e2b-bridge.js +4 -64
- package/dist/lsp/e2b-manager.js +6 -94
- package/dist/lsp/lsp-frames.js +0 -12
- package/dist/lsp/manager.js +4 -96
- package/dist/lsp/ws-transport.js +5 -55
- package/dist/lsp-evict.js +1 -15
- package/dist/main.js +57 -765
- package/dist/memory-bundle-engine.js +0 -55
- package/dist/memory-export.js +0 -4
- package/dist/memory-posture.js +1 -15
- package/dist/memory-scope.js +11 -148
- package/dist/memory-sync-client.js +2 -44
- package/dist/memory-sync.js +1 -80
- package/dist/model-select.js +3 -80
- package/dist/observability/cost-quota.js +1 -17
- package/dist/observability/cost-taxonomy.js +0 -34
- package/dist/observability/fail-open.js +7 -86
- package/dist/observability/logger.js +0 -6
- package/dist/observability/metrics.js +0 -94
- package/dist/observability/otel-exporter.js +3 -13
- package/dist/observability/principal-context.js +0 -9
- package/dist/observability/prompt-manifest.js +1 -37
- package/dist/observability/rate-limit.js +0 -4
- package/dist/observability/secret-env-scrub.js +2 -56
- package/dist/observability/tool-trace.js +1 -70
- package/dist/orchestration/hardened-vm-runner.js +4 -118
- package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
- package/dist/orchestration/hardened-vm-worker.js +0 -27
- package/dist/orchestration/subagent-steer.js +1 -45
- package/dist/orchestration/workflow-agent-steer.js +1 -80
- package/dist/orchestration/workflow-completion-inbox.js +32 -285
- package/dist/orchestration/workflow-notify-journal.js +16 -259
- package/dist/org-memory-admission.js +3 -47
- package/dist/parent-watch.js +2 -48
- package/dist/parked-decide.js +1 -109
- package/dist/per-task-image.js +0 -57
- package/dist/plan-cache-probe.js +3 -27
- package/dist/plugins/adoption-log-sql.js +2 -119
- package/dist/plugins/approval-ask-store-memory.js +3 -38
- package/dist/plugins/approval-ask-store-sql.js +6 -188
- package/dist/plugins/approval-exemption-store.js +2 -28
- package/dist/plugins/background-agent-store-sql.js +4 -105
- package/dist/plugins/background-shell-support.js +14 -122
- package/dist/plugins/blob-backend.js +6 -169
- package/dist/plugins/breaker-state-sql.js +8 -46
- package/dist/plugins/caching-session-store.js +4 -106
- package/dist/plugins/checkpoint-store-sql.js +18 -505
- package/dist/plugins/e2b-orphan-reclaim.js +0 -45
- package/dist/plugins/file-outcome-sink.js +0 -9
- package/dist/plugins/file-resume-anchor-store.js +4 -43
- package/dist/plugins/file-run-store.js +26 -364
- package/dist/plugins/file-snapshot-store-sql.js +10 -181
- package/dist/plugins/fork-routing-session-store.js +8 -111
- package/dist/plugins/host-platform.js +2 -91
- package/dist/plugins/image-bake-store-sql.js +7 -250
- package/dist/plugins/image-index-sql.js +4 -123
- package/dist/plugins/k8s-bg-scripts.js +4 -88
- package/dist/plugins/k8s-exec-protocol.js +0 -41
- package/dist/plugins/leader-run-store-sql.js +0 -101
- package/dist/plugins/local-checkpoint-store.js +8 -128
- package/dist/plugins/local-session-store.js +34 -296
- package/dist/plugins/local-task-attachment-store.js +2 -16
- package/dist/plugins/mailbox-store-sql.js +8 -65
- package/dist/plugins/memory-embedder-fingerprint.js +5 -166
- package/dist/plugins/memory-embedder.js +4 -70
- package/dist/plugins/memory-engine-pg.js +6 -170
- package/dist/plugins/memory-engine-tidb.js +7 -155
- package/dist/plugins/memory-engine-vector-util.js +0 -10
- package/dist/plugins/memory-key-guards.js +0 -34
- package/dist/plugins/memory-origin-law.js +0 -187
- package/dist/plugins/memory-resume-anchor-store.js +0 -17
- package/dist/plugins/memory-run-store.js +12 -89
- package/dist/plugins/memory-session-policy-store.js +0 -17
- package/dist/plugins/memory-sync-store-pg.js +4 -49
- package/dist/plugins/memory-sync-store-tidb.js +3 -35
- package/dist/plugins/outcome-ledger-sql.js +3 -97
- package/dist/plugins/permission-rule-store-file.js +5 -133
- package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
- package/dist/plugins/permission-rule-store-sql.js +50 -307
- package/dist/plugins/pg-cost-quota.js +0 -7
- package/dist/plugins/pg-pool.js +0 -92
- package/dist/plugins/pg-rate-limiter.js +2 -13
- package/dist/plugins/pg-safe-json.js +4 -40
- package/dist/plugins/pg-session-storage.js +25 -189
- package/dist/plugins/posix-shell-fs.js +1 -31
- package/dist/plugins/remote-env-adb.js +12 -101
- package/dist/plugins/remote-env-e2b.js +40 -371
- package/dist/plugins/remote-env-file-error.js +0 -37
- package/dist/plugins/remote-env-host.js +63 -483
- package/dist/plugins/remote-env-k8s.js +32 -326
- package/dist/plugins/remote-env-local-docker.js +24 -157
- package/dist/plugins/remote-env-ssh.js +20 -128
- package/dist/plugins/remote-scratchpad.js +2 -32
- package/dist/plugins/remote-shell.js +1 -32
- package/dist/plugins/resume-anchor-store-sql.js +0 -11
- package/dist/plugins/retention-lane-store-sql.js +0 -108
- package/dist/plugins/retention-store-sql.js +4 -383
- package/dist/plugins/roster-store-sql.js +0 -55
- package/dist/plugins/run-store-sql.js +14 -251
- package/dist/plugins/s3-presign.js +2 -49
- package/dist/plugins/scheduler-support.js +3 -80
- package/dist/plugins/send-file-ledger.js +4 -53
- package/dist/plugins/send-user-file.js +4 -94
- package/dist/plugins/session-placement.js +1 -89
- package/dist/plugins/session-policy-store-sql.js +3 -81
- package/dist/plugins/session-store.js +0 -57
- package/dist/plugins/shared-memory-store-sql.js +4 -167
- package/dist/plugins/sql-driver.js +0 -17
- package/dist/plugins/sql-errors.js +0 -7
- package/dist/plugins/sql-escape.js +0 -8
- package/dist/plugins/sql-row-helpers.js +0 -25
- package/dist/plugins/store-backend.js +42 -222
- package/dist/plugins/store-contracts.js +2 -46
- package/dist/plugins/task-attachment-store.js +3 -49
- package/dist/plugins/task-list-store-sql.js +0 -79
- package/dist/plugins/tidb-cost-quota.js +1 -4
- package/dist/plugins/tidb-pool.js +1 -207
- package/dist/plugins/tidb-rate-limiter.js +3 -9
- package/dist/plugins/tidb-session-storage.js +4 -70
- package/dist/plugins/tidb-session-store.js +19 -341
- package/dist/plugins/tool-result-store-sql.js +4 -179
- package/dist/plugins/usage-window-store-sql.js +0 -8
- package/dist/plugins/web-search.js +10 -132
- package/dist/plugins/workflow-journal-store-sql.js +2 -58
- package/dist/plugins/workflow-run-store-sql.js +6 -91
- package/dist/plugins/worktree-isolation.js +6 -126
- package/dist/plugins/write-behind-counter.js +16 -75
- package/dist/principal-jwt.js +5 -60
- package/dist/project-memory.js +15 -146
- package/dist/prompts-domain-validate.js +1 -51
- package/dist/question.js +2 -111
- package/dist/resource-suspend.js +0 -18
- package/dist/router/route-orchestration.js +0 -77
- package/dist/rules-consent.d.ts +44 -5
- package/dist/rules-consent.js +75 -215
- package/dist/run-local.js +14 -381
- package/dist/runs.js +29 -617
- package/dist/runtime-caps-resolver.js +6 -132
- package/dist/runtime-governance.js +1 -232
- package/dist/sandbox-pkg-source.js +0 -37
- package/dist/sealed-key.js +3 -68
- package/dist/security.js +6 -291
- package/dist/session-leaf-bus.js +0 -32
- package/dist/session-sync-content.js +1 -69
- package/dist/session-sync-kernel.js +3 -60
- package/dist/session-sync.js +3 -66
- package/dist/session-titler.js +7 -41
- package/dist/session-watch.js +9 -73
- package/dist/shared-memory-scope-authorizer.js +0 -18
- package/dist/sighup-idle.js +1 -12
- package/dist/spec-fields.js +4 -143
- package/dist/store-live-probe.js +3 -49
- package/dist/task-a2a.js +1 -126
- package/dist/task-cwd.js +1 -103
- package/dist/task-mcp.js +1 -89
- package/dist/task-settings.js +10 -300
- package/dist/task-workflow.js +8 -75
- package/dist/tool-approval.d.ts +28 -1
- package/dist/tool-approval.js +70 -1430
- package/dist/trace/artifacts.js +5 -20
- package/dist/trace/engine-notice-wire.js +6 -138
- package/dist/trace/ledger-sink.js +7 -96
- package/dist/trace/project.js +4 -448
- package/dist/trace/redact.js +9 -77
- package/dist/turn-activity.js +1 -27
- package/dist/usage-analytics.js +4 -34
- package/dist/wall-clock-jump-guard.js +1 -68
- package/package.json +3 -3
- package/dist/capabilities/scenario-alias.d.ts +0 -27
- package/dist/capabilities/scenario-alias.js +0 -61
|
@@ -1,43 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LOCAL session store — the DB-less twin of {@link TiDBSessionStore} (clay 2026-06-25 seamless local↔cloud). Wraps a
|
|
3
|
-
* core `SessionRepo` (in-memory `InMemorySessionRepo` for v1; `FileSessionRepo` for the file-backed follow-on — both
|
|
4
|
-
* expose create/open/list/delete/fork) and projects it into the service's `SessionStore` + `OwnerAwareSessionStore`
|
|
5
|
-
* shape, so a locally-run HTTP service serves the SAME session contract a cloud worker does: acquire (the run path) +
|
|
6
|
-
* the §0.5 ownership ops (ownerOf/register/fork/deleteSession).
|
|
7
|
-
*
|
|
8
|
-
* Single-user by construction: a local file/in-memory deployment has no multi-tenant owner column (core's
|
|
9
|
-
* SessionMetadata is `{id, createdAt}`), so REQUIRE_PRINCIPAL must be OFF (main.ts enforces this) and owner is tracked
|
|
10
|
-
* here only as the service-side scope (null/"local").
|
|
11
|
-
*
|
|
12
|
-
* P0.5 variant-2 (clay 2026-06-26): `listSessions` IS implemented — it enumerates the repo's persisted sessions
|
|
13
|
-
* (`repo.list()`) and synthesizes a {@link SessionSummary} per id from the SESSION abstraction (no run ledger), so
|
|
14
|
-
* GET /v1/sessions (server.ts: `sessionStorage.listSessions ?? runStore.listSessions`) takes the FIRST branch and the
|
|
15
|
-
* resume picker is restart-visible when the repo is the file-backed `FileSessionRepo`. Run-ledger-only fields
|
|
16
|
-
* (`runCount`/`lastStatus`) are honestly DEGRADED (the session abstraction has no runs); preview is synthesized from
|
|
17
|
-
* the last user-text entry. The in-memory `InMemorySessionRepo` works too (process-local; lost on restart) — the
|
|
18
|
-
* persistence comes from the repo, not this wrapper.
|
|
19
|
-
*/
|
|
20
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
21
2
|
import { join, resolve } from "node:path";
|
|
22
|
-
// 🔴 生产级兼容性 bug 撞获(2026-07-25,sema-desktop PoC 全新 npm install 复现):`Session` 在这个文件里
|
|
23
|
-
// 只当类型用(见下方 `as Session` 断言),此前却写成值导入——本地锁定 core@1.401.0 时 `Session` 恰好还是
|
|
24
|
-
// runtime 导出,掩盖了这个问题;但 core 1.403+(黑板 [1628]/[1634])把 `Session` 降级成纯类型、改用
|
|
25
|
-
// `StoredSession` 作 runtime class。任何新装 `@sema-agent/server` 在这行 import 会直接
|
|
26
|
-
// `SyntaxError: does not provide an export named 'Session'` 崩溃启动。改成 `import type` 消除。
|
|
27
|
-
// ✅ 尾声(design/158 F-5 核账,2026-07-28):上文历史版本提到的「三处 runtime `new Session(...)`
|
|
28
|
-
// 待迁移 + 依赖收紧 >=1.401.0 <1.403.0」**均已完成**——全仓 `new Session(` 现为零命中(三处都已迁
|
|
29
|
-
// `StoredSession`),依赖范围现为 `^1.444.0`。此注释保留前半段是因为「值导入 vs 类型导入被本地锁定
|
|
30
|
-
// 版本掩盖」这个缺陷机制仍值得后人读;宣称"待办"的部分已按实况改写(注释宣称≠代码实做是本仓
|
|
31
|
-
// 已立案的病族,过期待办比没有更有害)。
|
|
32
3
|
import { SessionError, validateEntriesForImport } from "@sema-agent/core";
|
|
33
4
|
import { contentForkRelation, fastForwardSharedContentDiverged, identicalIdsAlsoIdenticalContent } from "../session-sync-content.js";
|
|
34
5
|
import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
|
|
35
6
|
import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, } from "../session-sync-kernel.js";
|
|
36
7
|
const ownerEq = (a, b) => (a ?? null) === (b ?? null);
|
|
37
8
|
const isNotFound = (e) => e instanceof SessionError && e.code === "not_found";
|
|
38
|
-
/** Extract the plain text of a USER message entry (the objective preview source) — `null` for any other entry
|
|
39
|
-
* type or a non-user message, so {@link LocalSessionStore.previewOf} walks past tool/assistant turns to the last
|
|
40
|
-
* thing the human asked. content may be a bare string or a TextContent[] (join the text parts). */
|
|
41
9
|
function userText(entry) {
|
|
42
10
|
if (entry.type !== "message")
|
|
43
11
|
return null;
|
|
@@ -57,93 +25,35 @@ function userText(entry) {
|
|
|
57
25
|
}
|
|
58
26
|
return null;
|
|
59
27
|
}
|
|
60
|
-
/** 🔴 [1684] MODULE-level, keyed by the RESOLVED sidecar path: the authority is the file, so every store instance
|
|
61
|
-
* pointing at that file must share one view + one lock. Keyed by `resolve()`d path so `./x` and `x` collapse too. */
|
|
62
28
|
const TITLE_STATE_BY_PATH = new Map();
|
|
63
29
|
export class LocalSessionStore {
|
|
64
30
|
repo;
|
|
65
|
-
/** #270 车1:托管留存声明 —— **诚实的 none**。本店的行没有任何按期删除的腿(留存 lane 的三方法只实现在
|
|
66
|
-
* SQL 聚合店上),所以 locked policy + 本店 ⇒ core 的 `assertRetentionCapability` 拒启,这是契约的
|
|
67
|
-
* **正确行为**不是缺口(要留存治理先上 DB 后端;设计稿 §1「不做清单」逐字)。缺席也会被读成 none,
|
|
68
|
-
* 显式写出来是文档义务:沉默表达不出「我核对过我的介质就是删不了」。 */
|
|
69
31
|
retention = UNMANAGED_RETENTION;
|
|
70
|
-
/** In-flight acquisitions keyed by id, so concurrent acquire(sameId) in one process share one (mirrors TiDBSessionStore). */
|
|
71
32
|
pending = new Map();
|
|
72
|
-
/** Service-side owner scope per session (core's repo has no owner column). Registered-ownerless ⇒ null; absent ⇒ undefined. */
|
|
73
33
|
owners = new Map();
|
|
74
|
-
/** Last-activity marker (touch) for ordering — the keyset sort key for {@link listSessions}. PERSISTED to a JSON
|
|
75
|
-
* sidecar under {@link dataDir} (when given) so the /resume picker's newest-first order SURVIVES a service restart:
|
|
76
|
-
* without persistence the map starts EMPTY on boot and every session degrades to `createdAt` ordering (the picker
|
|
77
|
-
* shows the wrong "most recent" — TOC-review #10). In-memory only (no sidecar) when `dataDir` is absent (the
|
|
78
|
-
* process-local `InMemorySessionRepo` case, where the sessions themselves are lost on restart anyway). */
|
|
79
34
|
lastActivity = new Map();
|
|
80
|
-
/** K-5c (core 1.155 SessionStore.noteTaskRun seam): the latest task run per session, recorded by core's Runner at
|
|
81
|
-
* runTask START (so a running/suspended run surfaces too) → projected as `lastRunId` on listSessions. The cloud
|
|
82
|
-
* TiDB/PG twins derive it from `task_run`; the local backend has no runs ledger, so this is an in-memory map.
|
|
83
|
-
* ⚠️ IN-MEMORY BEST-EFFORT (honest degrade, NOT sidecar-persisted — review Q6): it covers the PRIMARY /resume use
|
|
84
|
-
* case (re-attach to a RUNNING run — recorded at the run's start, the process can't have restarted while that run
|
|
85
|
-
* is still live), and is LOST on a process restart. That's acceptable: a session whose last run predates a restart
|
|
86
|
-
* has only TERMINAL runs (no live tail to re-attach), so `lastRunId` falls back to null and the shell uses its
|
|
87
|
-
* no-anchor path — same posture as the other local degrades here (runCount:0, lastStatus:""). Persisting it would
|
|
88
|
-
* need a sidecar-format change (the #10 clobber area); deferred until a real local-file-backed need. */
|
|
89
35
|
lastTaskRun = new Map();
|
|
90
|
-
/** 2c P1d-β — live staged-import handles keyed by stagingId, so a local staging's in-memory buffer is readable via
|
|
91
|
-
* readStagedEntries (the durable twins re-read the staged session_event rows; local has no such rows). */
|
|
92
36
|
stagings = new Map();
|
|
93
|
-
/** Lazy one-shot load of the persisted lastActivity sidecar (memoized; only the first reader pays the read). */
|
|
94
37
|
loaded;
|
|
95
|
-
/** A pending debounced sidecar flush (coalesces a burst of touch() into one write). undefined ⇒ no flush queued. */
|
|
96
38
|
flushTimer;
|
|
97
|
-
/** Serializes sidecar writes (bug B): the in-flight write promise. A flush() while one is running CHAINS after it
|
|
98
|
-
* (re-checking the dirty flag) so an older snapshot can never rename after a newer one (lost-update). */
|
|
99
39
|
writeChain = Promise.resolve();
|
|
100
|
-
/** Set by touch/delete when the map mutated; cleared by a write that captured the change. A write that ran while this
|
|
101
|
-
* is set re-runs (a touch that landed during an active write isn't dropped). Drives the chain's re-run guard (bug B). */
|
|
102
40
|
dirty = false;
|
|
103
|
-
/** Monotonic per-write counter — feeds a UNIQUE tmp filename per write so overlapping writes can't share one tmp path
|
|
104
|
-
* and tear each other's bytes (bug B). */
|
|
105
41
|
writeSeq = 0;
|
|
106
|
-
/** Last time a sidecar-write error was warned (bug D): throttle the best-effort warn so a persistently-failing disk
|
|
107
|
-
* doesn't spam the log on every debounced flush. 0 ⇒ never warned. */
|
|
108
42
|
lastWarnAt = 0;
|
|
109
|
-
/** Absolute path to the lastActivity JSON sidecar, or undefined when not persisting (in-memory-only deployment). */
|
|
110
43
|
sidecarPath;
|
|
111
|
-
/** Auto-title map + its own LIGHT sidecar (`session-titles.json`). Separate file from the
|
|
112
|
-
* lastActivity sidecar ON PURPOSE (that format is a #10 clobber area); titles are write-once + ultra-low-rate
|
|
113
|
-
* (one per session, ever), so a plain read-modify-write with tmp+rename per write is enough — no debounce.
|
|
114
|
-
*
|
|
115
|
-
* 🔴 CROSS-INSTANCE (fixed 2026-07-25, blackboard [1684] — core's "per-instance state" defect class):
|
|
116
|
-
* these three used to be INSTANCE fields, which is exactly the shape core described — «the backend is shared
|
|
117
|
-
* more widely than the object, but the state and the lock hang off the object». Two `LocalSessionStore`s over
|
|
118
|
-
* the SAME dataDir each held their own `titles` map and their own write chain, so a real concurrency probe
|
|
119
|
-
* (`test/file-stores-cross-instance-concurrency.test.ts`) showed **BOTH winning** the write-once IS-NULL gate
|
|
120
|
-
* (`wins=2`): each checked its own map, both saw absent, both returned `true`, and the second whole-map
|
|
121
|
-
* snapshot silently clobbered the first title. That is core's A-class (CAS double-win) and B-class
|
|
122
|
-
* (lost update) stacked. Fix per core's prescription: state + lock live at MODULE level, keyed by the resolved
|
|
123
|
-
* sidecar path, so any number of instances over one dataDir collapse to a single authority.
|
|
124
|
-
* Sharing key = the resolved path; NO sidecar (in-memory deployment) ⇒ no shared backend exists ⇒ that case
|
|
125
|
-
* keeps genuinely per-instance state (see {@link titleState}). */
|
|
126
44
|
titleSidecarPath;
|
|
127
|
-
/** Per-instance fallback used ONLY when there is no sidecar path (nothing is shared, so nothing to collapse). */
|
|
128
45
|
ownTitleState = { titles: new Map(), writeChain: Promise.resolve() };
|
|
129
|
-
/** @param repo the core SessionRepo this store projects into the §0.5 SessionStore shape.
|
|
130
|
-
* @param dataDir the SAME data dir the file-backed repo persists under (LocalBackend passes `fileBackend.root`).
|
|
131
|
-
* When given, lastActivity is persisted to `<dataDir>/session-last-activity.json` so it survives a
|
|
132
|
-
* restart (the file-backed deployment); omit it for the in-memory deployment (nothing survives). */
|
|
133
46
|
constructor(repo, dataDir) {
|
|
134
47
|
this.repo = repo;
|
|
135
48
|
this.sidecarPath = dataDir ? join(dataDir, "session-last-activity.json") : undefined;
|
|
136
|
-
// [1684] resolve() 规范化后当共享键:`./x` 与 `x` 指同一文件,必须塌缩到同一权威。
|
|
137
49
|
this.titleSidecarPath = dataDir ? resolve(join(dataDir, "session-titles.json")) : undefined;
|
|
138
50
|
if (this.titleSidecarPath && !TITLE_STATE_BY_PATH.has(this.titleSidecarPath)) {
|
|
139
51
|
TITLE_STATE_BY_PATH.set(this.titleSidecarPath, { titles: new Map(), writeChain: Promise.resolve() });
|
|
140
52
|
}
|
|
141
53
|
}
|
|
142
|
-
/** 该实例应当使用的 title 状态:有 sidecar ⇒ 按路径共享的那份;无 sidecar ⇒ 自己那份(无共享后端)。 */
|
|
143
54
|
get titleState() {
|
|
144
55
|
return this.titleSidecarPath ? TITLE_STATE_BY_PATH.get(this.titleSidecarPath) : this.ownTitleState;
|
|
145
56
|
}
|
|
146
|
-
/** Lazy one-shot hydrate of the persisted titles (mirrors ensureLoaded's posture: missing file silent, torn warn). */
|
|
147
57
|
async ensureTitlesLoaded() {
|
|
148
58
|
if (!this.titleSidecarPath)
|
|
149
59
|
return;
|
|
@@ -155,7 +65,7 @@ export class LocalSessionStore {
|
|
|
155
65
|
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
156
66
|
for (const [id, t] of Object.entries(parsed)) {
|
|
157
67
|
if (typeof t === "string" && !st.titles.has(id))
|
|
158
|
-
st.titles.set(id, t);
|
|
68
|
+
st.titles.set(id, t);
|
|
159
69
|
}
|
|
160
70
|
}
|
|
161
71
|
catch (e) {
|
|
@@ -166,8 +76,6 @@ export class LocalSessionStore {
|
|
|
166
76
|
}
|
|
167
77
|
return st.loaded;
|
|
168
78
|
}
|
|
169
|
-
/** Chained snapshot write of the titles sidecar (the chain guarantees the LAST rename carries the
|
|
170
|
-
* NEWEST map state: each link re-snapshots the SHARED map at write time, so an overtaken writer is harmless). */
|
|
171
79
|
persistTitles() {
|
|
172
80
|
if (!this.titleSidecarPath)
|
|
173
81
|
return Promise.resolve();
|
|
@@ -185,15 +93,10 @@ export class LocalSessionStore {
|
|
|
185
93
|
});
|
|
186
94
|
return st.writeChain;
|
|
187
95
|
}
|
|
188
|
-
/** Cheap pre-LLM probe (local twin). The map write needs no session_meta row, so there is
|
|
189
|
-
* no "none" (register-lag) dimension here — two states only. */
|
|
190
96
|
async probeTitle(sessionId) {
|
|
191
97
|
await this.ensureTitlesLoaded();
|
|
192
98
|
return this.titleState.titles.has(sessionId) ? "titled" : "untitled";
|
|
193
99
|
}
|
|
194
|
-
/** Write-once auto-title (the local twin of the SQL setTitleIfNull). Persists the whole
|
|
195
|
-
* (small) map via a SERIALIZED tmp+rename snapshot; a write failure keeps the in-memory title (degrades to
|
|
196
|
-
* re-title after restart). */
|
|
197
100
|
async setTitleIfNull(sessionId, title) {
|
|
198
101
|
await this.ensureTitlesLoaded();
|
|
199
102
|
const st = this.titleState;
|
|
@@ -203,9 +106,6 @@ export class LocalSessionStore {
|
|
|
203
106
|
await this.persistTitles();
|
|
204
107
|
return true;
|
|
205
108
|
}
|
|
206
|
-
/** Best-effort, one-shot hydrate of the persisted lastActivity map (no-op when not persisting / no sidecar yet). A
|
|
207
|
-
* read/parse error degrades to an empty map (createdAt ordering) rather than failing the list — the picker still
|
|
208
|
-
* works, just without restart-persisted recency. Memoized so concurrent listSessions share one read. */
|
|
209
109
|
async ensureLoaded() {
|
|
210
110
|
if (!this.sidecarPath)
|
|
211
111
|
return;
|
|
@@ -216,14 +116,11 @@ export class LocalSessionStore {
|
|
|
216
116
|
const raw = await readFile(path, "utf8");
|
|
217
117
|
const parsed = JSON.parse(raw);
|
|
218
118
|
for (const [id, ts] of Object.entries(parsed)) {
|
|
219
|
-
// Don't clobber an in-process touch() that landed before the lazy load ran (live writes win over disk).
|
|
220
119
|
if (typeof ts === "string" && !this.lastActivity.has(id))
|
|
221
120
|
this.lastActivity.set(id, ts);
|
|
222
121
|
}
|
|
223
122
|
}
|
|
224
123
|
catch (e) {
|
|
225
|
-
// missing sidecar (first boot) is EXPECTED — silent; a torn file / permission error degrades to an empty map
|
|
226
|
-
// (createdAt ordering) but is WARNED (throttled, bug D) so disk-full/permission isn't hidden. Never throws.
|
|
227
124
|
if (e?.code !== "ENOENT")
|
|
228
125
|
this.warnSidecar("read", e);
|
|
229
126
|
}
|
|
@@ -231,59 +128,42 @@ export class LocalSessionStore {
|
|
|
231
128
|
}
|
|
232
129
|
return this.loaded;
|
|
233
130
|
}
|
|
234
|
-
/** Queue a debounced, best-effort atomic write of the lastActivity map to the sidecar (coalesces touch bursts). The
|
|
235
|
-
* write is fire-and-forget: a persistence failure must NEVER break the run path (touch is on the hot acquire path),
|
|
236
|
-
* so errors are swallowed — the worst case is a restart falling back to createdAt ordering for un-flushed touches. */
|
|
237
131
|
scheduleFlush() {
|
|
238
|
-
this.dirty = true;
|
|
132
|
+
this.dirty = true;
|
|
239
133
|
if (!this.sidecarPath || this.flushTimer)
|
|
240
134
|
return;
|
|
241
135
|
this.flushTimer = setTimeout(() => {
|
|
242
136
|
this.flushTimer = undefined;
|
|
243
137
|
void this.flush();
|
|
244
138
|
}, 250);
|
|
245
|
-
this.flushTimer.unref?.();
|
|
246
|
-
}
|
|
247
|
-
/** Atomic best-effort write of the lastActivity map. SERIALIZED via {@link writeChain} (bug B): overlapping flushes
|
|
248
|
-
* chain instead of racing, so an older snapshot can never rename after a newer one (lost-update), and a unique tmp
|
|
249
|
-
* name per write means two writes can't tear each other's bytes. Returns the chain so callers (dispose) can await the
|
|
250
|
-
* final write (bug C). Each link HYDRATES the disk sidecar first (bug A) and writes the UNION of disk + in-process —
|
|
251
|
-
* the `has(id)` live-wins guard in ensureLoaded() keeps in-process touches authoritative — so a fresh-boot flush
|
|
252
|
-
* before any list never CLOBBERS the prior boot's recency for not-yet-touched sessions. */
|
|
139
|
+
this.flushTimer.unref?.();
|
|
140
|
+
}
|
|
253
141
|
flush() {
|
|
254
142
|
if (!this.sidecarPath)
|
|
255
143
|
return Promise.resolve();
|
|
256
144
|
const next = this.writeChain.then(() => this.doFlush());
|
|
257
|
-
this.writeChain = next.catch(() => { });
|
|
145
|
+
this.writeChain = next.catch(() => { });
|
|
258
146
|
return next;
|
|
259
147
|
}
|
|
260
|
-
/** One serialized sidecar write: hydrate-then-merge (bug A), snapshot, atomic tmp+rename with a UNIQUE tmp name (bug
|
|
261
|
-
* B). Clears {@link dirty} for the change it captured; re-runs once if a touch landed mid-write so it's not dropped. */
|
|
262
148
|
async doFlush() {
|
|
263
149
|
const path = this.sidecarPath;
|
|
264
150
|
if (!path)
|
|
265
151
|
return;
|
|
266
|
-
// bug A: merge the on-disk sidecar BEFORE snapshotting so we never overwrite another boot's recency with an empty
|
|
267
|
-
// (or partial) in-process map. ensureLoaded()'s `!has(id)` guard keeps live in-process touches authoritative.
|
|
268
152
|
await this.ensureLoaded();
|
|
269
|
-
this.dirty = false;
|
|
153
|
+
this.dirty = false;
|
|
270
154
|
const snapshot = Object.fromEntries(this.lastActivity);
|
|
271
155
|
try {
|
|
272
156
|
await mkdir(join(path, ".."), { recursive: true });
|
|
273
|
-
// bug B: unique tmp per write (pid + monotonic counter) — overlapping/successive writes can't share one tmp path.
|
|
274
157
|
const tmp = `${path}.${process.pid}.${++this.writeSeq}.tmp`;
|
|
275
158
|
await writeFile(tmp, JSON.stringify(snapshot), "utf8");
|
|
276
|
-
await rename(tmp, path);
|
|
159
|
+
await rename(tmp, path);
|
|
277
160
|
}
|
|
278
161
|
catch (e) {
|
|
279
|
-
this.warnSidecar("write", e);
|
|
280
|
-
// best-effort: a persistence failure degrades restart-recency, never breaks the run path.
|
|
162
|
+
this.warnSidecar("write", e);
|
|
281
163
|
}
|
|
282
164
|
if (this.dirty)
|
|
283
|
-
await this.doFlush();
|
|
165
|
+
await this.doFlush();
|
|
284
166
|
}
|
|
285
|
-
/** Throttled best-effort warn for a sidecar I/O failure (bug D): never throws, at most once per ~minute so a
|
|
286
|
-
* persistently-failing disk doesn't spam the log on every debounced flush. */
|
|
287
167
|
warnSidecar(op, e) {
|
|
288
168
|
const now = Date.now();
|
|
289
169
|
if (now - this.lastWarnAt < 60_000)
|
|
@@ -298,9 +178,6 @@ export class LocalSessionStore {
|
|
|
298
178
|
this.owners.set(id, null);
|
|
299
179
|
return { session: session, sessionId: id };
|
|
300
180
|
}
|
|
301
|
-
// design/114 Phase3 (F1): a `requireExisting` acquire NEVER creates and can REJECT (not_found) — it MUST bypass
|
|
302
|
-
// the shared create-dedup `pending`, else a concurrent create-capable waiter awaiting that same promise would
|
|
303
|
-
// inherit the not_found rejection instead of creating-on-miss. (Twin of TiDBSessionStore.acquire.)
|
|
304
181
|
if (opts?.requireExisting)
|
|
305
182
|
return this.load(sessionId, true);
|
|
306
183
|
const inflight = this.pending.get(sessionId);
|
|
@@ -311,8 +188,6 @@ export class LocalSessionStore {
|
|
|
311
188
|
return p;
|
|
312
189
|
}
|
|
313
190
|
async load(sessionId, requireExisting = false) {
|
|
314
|
-
// open() throws SessionError("not_found") for an unknown id — create it ownerless then (a caller-chosen brand-new
|
|
315
|
-
// id the service didn't pre-register; only the caller who chose it can reach it, and it holds no prior data).
|
|
316
191
|
try {
|
|
317
192
|
const session = await this.repo.open({ id: sessionId, createdAt: "" });
|
|
318
193
|
if (!this.owners.has(sessionId))
|
|
@@ -322,28 +197,17 @@ export class LocalSessionStore {
|
|
|
322
197
|
catch (e) {
|
|
323
198
|
if (!isNotFound(e))
|
|
324
199
|
throw e;
|
|
325
|
-
// design/114 Phase3: `requireExisting` ⇒ FAIL LOUD on a genuinely missing session, never the silent
|
|
326
|
-
// create-on-miss. Re-throw the store's own `not_found` SessionError; core maps it to `resume.session_not_found`.
|
|
327
200
|
if (requireExisting)
|
|
328
201
|
throw e;
|
|
329
202
|
const session = await this.repo.create({ id: sessionId });
|
|
330
|
-
// GUARD: mirror the open branch — NEVER clobber an owner a prior register() recorded
|
|
331
|
-
// (the TiDB twin's load() re-registers ownerless via an idempotent no-op upsert). Without the guard, a
|
|
332
|
-
// register(id, principal)-then-acquire(id) [the §0.5 auth flow] would reset owner→null on first acquire →
|
|
333
|
-
// ownership gate goes silent → cross-principal attach. (register() now also creates the repo session, so this
|
|
334
|
-
// catch only fires for a genuinely-unregistered id — but guard it defensively regardless.)
|
|
335
203
|
if (!this.owners.has(sessionId))
|
|
336
204
|
this.owners.set(sessionId, null);
|
|
337
205
|
return { session: session, sessionId };
|
|
338
206
|
}
|
|
339
207
|
}
|
|
340
|
-
/** Idempotently record a session's owner — never overwrites an existing owner (mirrors TiDBSessionStore.register).
|
|
341
|
-
* TiDB parity: register INSERTs a session_meta row so the session EXISTS (forkable) even before its
|
|
342
|
-
* first acquire/run — so we also ensure an EMPTY repo session exists (open if present → never overwrite history;
|
|
343
|
-
* create if not), else `ownerOf` would say a registered-but-never-acquired session exists while `fork` 404s it. */
|
|
344
208
|
async register(sessionId, owner) {
|
|
345
209
|
if (this.owners.has(sessionId))
|
|
346
|
-
return;
|
|
210
|
+
return;
|
|
347
211
|
this.owners.set(sessionId, owner);
|
|
348
212
|
try {
|
|
349
213
|
await this.repo.open({ id: sessionId, createdAt: "" });
|
|
@@ -355,13 +219,6 @@ export class LocalSessionStore {
|
|
|
355
219
|
throw e;
|
|
356
220
|
}
|
|
357
221
|
}
|
|
358
|
-
/** Owner of a session: `null` if registered ownerless, `undefined` if no such session (mirrors TiDBSessionStore.ownerOf).
|
|
359
|
-
*
|
|
360
|
-
* S3 修(clay 裁 2026-07-26:**local=单用户,多租户走云** ⇒ 跨重启不承诺 owner 隔离):`owners` 是纯内存
|
|
361
|
-
* Map,重启后失忆——曾把「失忆」答成 `undefined`(=「无此会话」),让 DELETE /v1/sessions 的 route 门短路成
|
|
362
|
-
* already-gone、checkpoint 的 EXISTS 镜像门 return 0 ⇒ **E21 对盘上老会话整体 no-op 且无人知道**。
|
|
363
|
-
* 现在 Map miss 时探 repo:盘上在 ⇒ 按 load() 两处懒回填早已写下的同一判据回填 `null`(registered
|
|
364
|
-
* ownerless);repo 真没有才 `undefined`。SQL 孪生无此形(owner 在 session_meta 行,重启无损)。 */
|
|
365
222
|
async ownerOf(sessionId) {
|
|
366
223
|
if (this.owners.has(sessionId))
|
|
367
224
|
return this.owners.get(sessionId) ?? null;
|
|
@@ -376,30 +233,23 @@ export class LocalSessionStore {
|
|
|
376
233
|
throw e;
|
|
377
234
|
}
|
|
378
235
|
}
|
|
379
|
-
/** [1196] 六轮复审:owner+leafId 原子快照——单线程 JS 内两同步/顺序读之间无 await 打断 owner 判定
|
|
380
|
-
* (owner 先取快照,leaf 读后 owner 复核,变了=undefined 视作删除中,调用方按删处理)。 */
|
|
381
236
|
async getHead(sessionId) {
|
|
382
|
-
// S3 同根:Map miss ≠ 无此会话 —— 经 ownerOf 的 repo 探在腿判定(盘上在 ⇒ 回填 ownerless 后继续)。
|
|
383
237
|
if (!this.owners.has(sessionId) && (await this.ownerOf(sessionId)) === undefined)
|
|
384
238
|
return undefined;
|
|
385
239
|
const owner = this.owners.get(sessionId) ?? null;
|
|
386
240
|
const leafId = await this.getLeafId(sessionId);
|
|
387
241
|
if (!this.owners.has(sessionId) || (this.owners.get(sessionId) ?? null) !== owner)
|
|
388
|
-
return undefined;
|
|
242
|
+
return undefined;
|
|
389
243
|
return { owner, leafId };
|
|
390
244
|
}
|
|
391
|
-
/** 2c P1d-β — re-stamp the service-side owner scope WITHOUT rewriting entries (the staged-commit `identical` path:
|
|
392
|
-
* the log is already present & equal, so only the owner needs re-stamping to the importing principal, §9). */
|
|
393
245
|
restampOwner(sessionId, owner) {
|
|
394
246
|
if (this.owners.has(sessionId))
|
|
395
247
|
this.owners.set(sessionId, owner);
|
|
396
248
|
}
|
|
397
249
|
async touch(sessionId) {
|
|
398
250
|
this.lastActivity.set(sessionId, new Date().toISOString());
|
|
399
|
-
this.scheduleFlush();
|
|
251
|
+
this.scheduleFlush();
|
|
400
252
|
}
|
|
401
|
-
/** E18 — the session's current leaf SessionTreeEntry.id (@see TiDBSessionStore.getLeafId). Reads the repo's storage
|
|
402
|
-
* leaf (no acquire-lock; InMemory/File repo reads don't lock). null when the session doesn't exist / has no leaf. */
|
|
403
253
|
async getLeafId(sessionId) {
|
|
404
254
|
try {
|
|
405
255
|
const session = await this.repo.open({ id: sessionId, createdAt: "" });
|
|
@@ -411,40 +261,18 @@ export class LocalSessionStore {
|
|
|
411
261
|
throw e;
|
|
412
262
|
}
|
|
413
263
|
}
|
|
414
|
-
/** Durable history is retained by the repo; just drop the in-flight cache entry (mirrors TiDBSessionStore.release). */
|
|
415
264
|
async release(sessionId) {
|
|
416
265
|
this.pending.delete(sessionId);
|
|
417
266
|
}
|
|
418
|
-
/**
|
|
419
|
-
* P0.5 variant-2 (E16, clay 2026-06-26) — enumerate the caller's sessions (CC /resume picker) from the SESSION
|
|
420
|
-
* abstraction (`repo.list()`), keyset-paginated newest-first by last activity. Session IDENTITY comes from the
|
|
421
|
-
* repo (one entry per persisted session); a {@link SessionSummary} is SYNTHESIZED per id so the wire shape is
|
|
422
|
-
* byte-identical to {@link TiDBSessionStore.listSessions} (the run picker doesn't drift):
|
|
423
|
-
* - `owner` — the service-side owner map (process-local; null when unknown/ownerless). Owner FILTERING
|
|
424
|
-
* is best-effort here (the in-memory map IS the source of truth; a single-user local
|
|
425
|
-
* deployment runs REQUIRE_PRINCIPAL=off so the principal path passes owner=null and every
|
|
426
|
-
* null-owner session lists). The route already owner-gates at the HTTP layer.
|
|
427
|
-
* - `firstActivityAt` — the session's `createdAt` (repo metadata).
|
|
428
|
-
* - `lastActivityAt` — the touch marker (falls back to `createdAt`); the keyset sort key.
|
|
429
|
-
* - `objectivePreview`— the last USER-text entry, synthesized from the session tree (no run ledger).
|
|
430
|
-
* - `runCount`/`lastStatus` — DEGRADED (0 / ""): the session abstraction has no runs ledger; the TiDB twin
|
|
431
|
-
* LEFT-JOINs `task_run` for these, which the local backend (MemoryRunStore, separate
|
|
432
|
-
* ledger) intentionally does NOT correlate here (variant-2 is session-abstraction-only).
|
|
433
|
-
*
|
|
434
|
-
* Keyset matches the contract: STRICTLY older than the cursor, ties broken by id DESC; `limit` is honored after
|
|
435
|
-
* the in-memory sort. Cheap (a local single-user store holds few sessions). Errors reading one session's entries
|
|
436
|
-
* degrade that row's preview to null rather than failing the whole list.
|
|
437
|
-
*/
|
|
438
267
|
async listSessions(opts) {
|
|
439
|
-
await this.ensureLoaded();
|
|
268
|
+
await this.ensureLoaded();
|
|
440
269
|
await this.ensureTitlesLoaded();
|
|
441
270
|
const metas = await this.repo.list();
|
|
442
|
-
// Build a summary per persisted session, owner from the process-local map (degrade to null when unknown).
|
|
443
271
|
const all = [];
|
|
444
272
|
for (const meta of metas) {
|
|
445
273
|
const owner = this.owners.has(meta.id) ? (this.owners.get(meta.id) ?? null) : null;
|
|
446
274
|
if (opts.owner !== undefined && owner !== opts.owner && !(opts.includeUnowned === true && owner === null))
|
|
447
|
-
continue;
|
|
275
|
+
continue;
|
|
448
276
|
const firstActivityAt = meta.createdAt || new Date(0).toISOString();
|
|
449
277
|
const lastActivityAt = this.lastActivity.get(meta.id) ?? firstActivityAt;
|
|
450
278
|
all.push({
|
|
@@ -452,43 +280,31 @@ export class LocalSessionStore {
|
|
|
452
280
|
owner,
|
|
453
281
|
lastActivityAt,
|
|
454
282
|
firstActivityAt,
|
|
455
|
-
runCount: 0,
|
|
283
|
+
runCount: 0,
|
|
456
284
|
objectivePreview: await this.previewOf(meta),
|
|
457
|
-
lastStatus: "",
|
|
458
|
-
lastRunId: this.lastTaskRun.get(meta.id) ?? null,
|
|
459
|
-
title: this.titleState.titles.get(meta.id) ?? null,
|
|
285
|
+
lastStatus: "",
|
|
286
|
+
lastRunId: this.lastTaskRun.get(meta.id) ?? null,
|
|
287
|
+
title: this.titleState.titles.get(meta.id) ?? null,
|
|
460
288
|
});
|
|
461
289
|
}
|
|
462
|
-
// `?q=` — the local twin matches the synthesized objectivePreview (the last user text; the
|
|
463
|
-
// SQL twins match ANY run's preview via the runs ledger — each backend searches what it durably has).
|
|
464
|
-
// Filter BEFORE the sort/keyset slice so pagination stays correct.
|
|
465
290
|
const qlc = opts.q?.toLowerCase();
|
|
466
291
|
const filtered = qlc ? all.filter((r) => r.objectivePreview?.toLowerCase().includes(qlc)) : all;
|
|
467
|
-
// newest-first by (lastActivityAt, sessionId) — same ORDER BY the SQL twin emits.
|
|
468
292
|
filtered.sort((a, b) => a.lastActivityAt !== b.lastActivityAt
|
|
469
293
|
? (a.lastActivityAt < b.lastActivityAt ? 1 : -1)
|
|
470
294
|
: (a.sessionId < b.sessionId ? 1 : -1));
|
|
471
|
-
// keyset: keep rows STRICTLY older than the cursor (ties by id DESC), then honor limit (mirrors the SQL WHERE).
|
|
472
295
|
const c = opts.cursor;
|
|
473
296
|
const after = c
|
|
474
297
|
? filtered.filter((r) => r.lastActivityAt < c.lastActivityAt || (r.lastActivityAt === c.lastActivityAt && r.sessionId < c.sessionId))
|
|
475
298
|
: filtered;
|
|
476
299
|
return after.slice(0, opts.limit);
|
|
477
300
|
}
|
|
478
|
-
/** K-5c (core 1.155 SessionStore seam): core's Runner calls this at runTask START with the run's taskId, so the
|
|
479
|
-
* latest run surfaces as `lastRunId` on listSessions even while it is still running/suspended (the local backend
|
|
480
|
-
* has no runs ledger to derive it from, unlike the TiDB/PG twins). In-memory, last-write-wins per session. */
|
|
481
301
|
noteTaskRun(sessionId, taskId) {
|
|
482
302
|
this.lastTaskRun.set(sessionId, taskId);
|
|
483
303
|
}
|
|
484
|
-
/** Synthesize a session's objective preview = the most-recent USER text message (the objective the run last saw).
|
|
485
|
-
* Reads the session's entries through the repo; any read error degrades to null (never fails the list). */
|
|
486
304
|
async previewOf(meta) {
|
|
487
305
|
try {
|
|
488
306
|
const session = await this.repo.open({ id: meta.id, createdAt: meta.createdAt });
|
|
489
307
|
const entries = await session.getEntries();
|
|
490
|
-
// Walk newest-first; the most recent USER message is the objective (mirrors task_run.objective_preview). A
|
|
491
|
-
// session with no user turn yet (e.g. registered-but-never-run) → null preview (honest empty, like the picker).
|
|
492
308
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
493
309
|
const text = userText(entries[i]);
|
|
494
310
|
if (text)
|
|
@@ -497,12 +313,9 @@ export class LocalSessionStore {
|
|
|
497
313
|
return null;
|
|
498
314
|
}
|
|
499
315
|
catch {
|
|
500
|
-
// best-effort: a missing/torn/unreadable session lists without a preview rather than failing the whole picker.
|
|
501
316
|
return null;
|
|
502
317
|
}
|
|
503
318
|
}
|
|
504
|
-
/** E17 — fork a session's WHOLE history to a NEW id owned by `owner` (core SessionRepo.fork). Returns the new id,
|
|
505
|
-
* or null if the source does not exist (the route maps that to 404). */
|
|
506
319
|
async fork(sourceId, owner) {
|
|
507
320
|
try {
|
|
508
321
|
const session = await this.repo.fork({ id: sourceId, createdAt: "" }, {});
|
|
@@ -516,104 +329,60 @@ export class LocalSessionStore {
|
|
|
516
329
|
throw e;
|
|
517
330
|
}
|
|
518
331
|
}
|
|
519
|
-
/** 2c session-sync — EXPORT a session's full durable entry log (core `SessionRepo.exportEntries`; the
|
|
520
|
-
* in-memory/file repo holds the whole log so there is no floor to bypass). null if the source is unknown / the
|
|
521
|
-
* underlying repo lacks the optional seam. The owner is NOT exported — import re-stamps it. */
|
|
522
332
|
async exportEntries(sessionId) {
|
|
523
333
|
if (!this.repo.exportEntries)
|
|
524
|
-
return null;
|
|
334
|
+
return null;
|
|
525
335
|
try {
|
|
526
336
|
return await this.repo.exportEntries(sessionId);
|
|
527
337
|
}
|
|
528
338
|
catch (e) {
|
|
529
339
|
if (isNotFound(e))
|
|
530
|
-
return null;
|
|
340
|
+
return null;
|
|
531
341
|
throw e;
|
|
532
342
|
}
|
|
533
343
|
}
|
|
534
|
-
/** 2c session-sync P1d-α (PULL streaming) — the IDS-ONLY projection of the full durable log (@see
|
|
535
|
-
* TiDBSessionStore.listEntryIds). The local repo holds the whole log in memory/file so there is no floor to bypass;
|
|
536
|
-
* ids are the `exportEntries` ids in order. null if the source is unknown / the repo lacks the export seam. */
|
|
537
344
|
async listEntryIds(sessionId) {
|
|
538
345
|
const entries = await this.exportEntries(sessionId);
|
|
539
346
|
return entries === null ? null : entries.map((e) => e.id);
|
|
540
347
|
}
|
|
541
|
-
/** 2c session-sync P1d-α (PULL streaming) — STREAM the full durable log as an async generator (@see
|
|
542
|
-
* TiDBSessionStore.exportEntriesStream). The LOCAL twin has no cross-instance buffering hazard (an in-memory map /
|
|
543
|
-
* single-process file repo), so it reads the whole log via the repo's `exportEntries` then yields it lazily, SLICED
|
|
544
|
-
* after `opts.afterSeq` (the LOCAL backend's native seq = the 0-based dense ARRAY INDEX of the entry in the
|
|
545
|
-
* oldest-first log; afterSeq=k → entries at index > k = `slice(k+1)`). afterSeq is a BACKEND-NATIVE seq cursor (a
|
|
546
|
-
* resume is always same-backend), so the local 0-based-index base needn't match the durable backends' `seq` column
|
|
547
|
-
* base. Resolves to null (NOT an empty iterable) for an unknown session — mirrors {@link exportEntries}.
|
|
548
|
-
* `opts.batchSize` is accepted for API parity but has no effect locally (the whole log is already in hand). */
|
|
549
348
|
async exportEntriesStream(sessionId, opts) {
|
|
550
349
|
const entries = await this.exportEntries(sessionId);
|
|
551
350
|
if (entries === null)
|
|
552
|
-
return null;
|
|
553
|
-
const afterSeq = opts?.afterSeq ?? -1;
|
|
554
|
-
const slice = entries.slice(afterSeq + 1);
|
|
351
|
+
return null;
|
|
352
|
+
const afterSeq = opts?.afterSeq ?? -1;
|
|
353
|
+
const slice = entries.slice(afterSeq + 1);
|
|
555
354
|
async function* gen() {
|
|
556
355
|
for (const e of slice)
|
|
557
356
|
yield e;
|
|
558
357
|
}
|
|
559
358
|
return gen();
|
|
560
359
|
}
|
|
561
|
-
/** 2c session-sync — IMPORT a verbatim entry log into `sessionId` via core `SessionRepo.importEntries`
|
|
562
|
-
* (which runs the `validateEntriesForImport` fail-closed gate before writing). `owner` is the authenticated
|
|
563
|
-
* importing principal (re-stamped into the service-side owner map; core's local repo has no owner column, §9).
|
|
564
|
-
* `owner=null` (single-user local) is passed as `undefined` to the core seam (its owner param is `string | undefined`). */
|
|
565
360
|
async importEntries(sessionId, owner, entries) {
|
|
566
361
|
if (!this.repo.importEntries)
|
|
567
362
|
throw new SessionError("storage", "session import is not supported by this local backend");
|
|
568
|
-
await this.repo.importEntries(sessionId, owner ?? undefined, entries);
|
|
569
|
-
this.owners.set(sessionId, owner);
|
|
570
|
-
}
|
|
571
|
-
/** 2c session-sync (§7/§8) — IDEMPOTENT REPLACE. The local twin's replace is DELETE-then-IMPORT: core's
|
|
572
|
-
* InMemorySessionRepo / FileSessionRepo expose no single replace primitive, but the local backend is single-writer
|
|
573
|
-
* with NO compaction floor / cross-instance race (a process-local in-memory map / a single-process file repo), so
|
|
574
|
-
* deleteSession-then-importEntries is effectively atomic for the local case (no concurrent reader can observe the
|
|
575
|
-
* gap, unlike the durable cross-instance backends which do it in ONE SQL txn). Both halves are best-effort feature-
|
|
576
|
-
* gated: a repo lacking either seam throws the same clear error importEntries does (no silent half-write). */
|
|
363
|
+
await this.repo.importEntries(sessionId, owner ?? undefined, entries);
|
|
364
|
+
this.owners.set(sessionId, owner);
|
|
365
|
+
}
|
|
577
366
|
async replaceEntries(sessionId, owner, entries) {
|
|
578
367
|
if (!this.repo.importEntries)
|
|
579
368
|
throw new SessionError("storage", "session import is not supported by this local backend");
|
|
580
369
|
if (!this.repo.delete)
|
|
581
370
|
throw new SessionError("storage", "session replace is not supported by this local backend (no delete seam)");
|
|
582
|
-
// VALIDATE-FIRST (before the delete): the durable twins run validateEntriesForImport INSIDE a txn that ROLLs BACK on
|
|
583
|
-
// a bad import, so a failed import can't destroy the existing session. The local twin is delete-then-import (no
|
|
584
|
-
// single-writer txn), so an invalid bundle — the COMMON failure — must be rejected BEFORE deleteSession purges the
|
|
585
|
-
// prior session; otherwise a bad import would purge-then-fail and the existing session would be gone. (A post-delete
|
|
586
|
-
// fs I/O failure on importEntries remains a residual single-writer risk the durable twins don't have — noted.)
|
|
587
371
|
validateEntriesForImport(entries);
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
await this.deleteSession(sessionId, owner); // owner-guarded purge (defense-in-depth, single-user: owner=null matches)
|
|
592
|
-
await this.importEntries(sessionId, owner, entries); // core gate runs inside + re-stamps the owner scope
|
|
593
|
-
}
|
|
594
|
-
/** 2c session-sync P1d-β (PUSH streaming) — begin a STAGED import. The LOCAL backend is single-writer with no
|
|
595
|
-
* cross-instance / cross-replica concurrency story (the durable twins carry that), so the staging handle simply
|
|
596
|
-
* ACCUMULATES the streamed batches in memory then, on commit, runs the SAME validate-first → replaceEntries the
|
|
597
|
-
* whole-bundle local import uses (effectively atomic locally — no concurrent reader can observe the gap). Keep it
|
|
598
|
-
* simple per §1: the durable twins are where the SHADOW-id swap + bounded-memory + concurrency matter. */
|
|
372
|
+
await this.deleteSession(sessionId, owner);
|
|
373
|
+
await this.importEntries(sessionId, owner, entries);
|
|
374
|
+
}
|
|
599
375
|
beginImportStaging(realSessionId, token) {
|
|
600
376
|
const h = new LocalStagingHandle(this, realSessionId, stagingIdFor(realSessionId, token));
|
|
601
377
|
this.stagings.set(h.stagingId, h);
|
|
602
378
|
return h;
|
|
603
379
|
}
|
|
604
|
-
/** 2c P1d-β (staged-row inspection) — the in-memory staged entries for a local staging id. No longer the import gate
|
|
605
|
-
* (the per-line StreamingImportValidator validates during the Phase-B stream); a tests/diagnostics seam. [] for an
|
|
606
|
-
* unknown/finished staging id (mirrors the durable twins' empty-set return). */
|
|
607
380
|
async readStagedEntries(stagingId) {
|
|
608
381
|
return this.stagings.get(stagingId)?.snapshotEntries() ?? [];
|
|
609
382
|
}
|
|
610
|
-
/** Drop a finished/aborted local staging handle from the registry (called by the handle on commit/abort). */
|
|
611
383
|
forgetStaging(stagingId) {
|
|
612
384
|
this.stagings.delete(stagingId);
|
|
613
385
|
}
|
|
614
|
-
/** E21 — purge a session's conversation history (core SessionRepo.delete), owner-guarded (single-user: owner=null
|
|
615
|
-
* matches). Idempotent: a missing/wrong-owner session is a no-op returning false. The runs-ledger / checkpoint /
|
|
616
|
-
* tool-result rows are purged separately by the route's purgeSession coordinator. */
|
|
617
386
|
async deleteSession(sessionId, owner) {
|
|
618
387
|
const known = this.owners.has(sessionId);
|
|
619
388
|
if (!known || !ownerEq(this.owners.get(sessionId) ?? null, owner))
|
|
@@ -627,50 +396,30 @@ export class LocalSessionStore {
|
|
|
627
396
|
}
|
|
628
397
|
this.owners.delete(sessionId);
|
|
629
398
|
this.lastActivity.delete(sessionId);
|
|
630
|
-
this.lastTaskRun.delete(sessionId);
|
|
631
|
-
// and a re-acquire(sameId) would surface the PRIOR incarnation's lastRunId (stale anchor).
|
|
632
|
-
// Drop the auto-title too (E21 parity with the SQL twins' session_meta row delete) and
|
|
633
|
-
// persist the removal through the SAME serialized chain (an in-flight grant snapshot must not rename
|
|
634
|
-
// AFTER this removal and resurrect it — the chain re-snapshots the map, so the delete always wins).
|
|
635
|
-
// 🔴 hydrate FIRST (workflow audit 2026-07-13): after a restart the map starts empty — an un-hydrated
|
|
636
|
-
// delete() returns false and the sidecar keeps the title (an E21 right-to-delete residue that would
|
|
637
|
-
// RESURRECT onto a same-id re-import). ensureTitlesLoaded is memoized — first caller pays, rest no-op.
|
|
399
|
+
this.lastTaskRun.delete(sessionId);
|
|
638
400
|
await this.ensureTitlesLoaded();
|
|
639
401
|
if (this.titleState.titles.delete(sessionId))
|
|
640
402
|
await this.persistTitles();
|
|
641
|
-
this.scheduleFlush();
|
|
403
|
+
this.scheduleFlush();
|
|
642
404
|
return true;
|
|
643
405
|
}
|
|
644
406
|
get size() {
|
|
645
407
|
return this.pending.size;
|
|
646
408
|
}
|
|
647
|
-
/** Graceful shutdown: AWAIT the final sidecar write (bug C) so a restart doesn't lose the last debounced-but-unwritten
|
|
648
|
-
* touch. Cancels any queued debounce, then awaits a flush (which chains after any in-flight write and merges disk). */
|
|
649
409
|
async dispose() {
|
|
650
410
|
this.pending.clear();
|
|
651
411
|
if (this.flushTimer) {
|
|
652
412
|
clearTimeout(this.flushTimer);
|
|
653
413
|
this.flushTimer = undefined;
|
|
654
414
|
}
|
|
655
|
-
// Always do a final flush when persisting (a pending debounce OR an in-flight write may hold the last touch); when
|
|
656
|
-
// not persisting it's a cheap no-op. AWAIT it (and the chain) so close() can't release the data-dir lock first.
|
|
657
415
|
if (this.sidecarPath)
|
|
658
416
|
await this.flush();
|
|
659
417
|
}
|
|
660
418
|
}
|
|
661
|
-
/**
|
|
662
|
-
* 2c session-sync P1d-β — the LOCAL {@link StagingHandle}. Unlike the durable twins (which stage into a shadow
|
|
663
|
-
* session_event partition + atomically rename it), the local backend is single-writer with no cross-instance race, so
|
|
664
|
-
* this handle ACCUMULATES the streamed batches in memory (idempotent on entry id — a re-streamed batch is deduped) and,
|
|
665
|
-
* on commit, runs the SAME validate-first → replaceEntries the whole-bundle local import uses (the existing
|
|
666
|
-
* delete-then-import that is effectively atomic for the single-process local case). It still re-classifies AT COMMIT
|
|
667
|
-
* (against the dst's current ids) so an unconsented fork/stale is refused, matching the durable twins' contract.
|
|
668
|
-
*/
|
|
669
419
|
class LocalStagingHandle {
|
|
670
420
|
store;
|
|
671
421
|
realSessionId;
|
|
672
422
|
stagingId;
|
|
673
|
-
/** Accumulated staged entries, deduped by entry id (resume re-streams from the start → dedup keeps it idempotent). */
|
|
674
423
|
staged = [];
|
|
675
424
|
seen = new Set();
|
|
676
425
|
constructor(store, realSessionId, stagingId) {
|
|
@@ -681,47 +430,36 @@ class LocalStagingHandle {
|
|
|
681
430
|
async appendBatch(entries) {
|
|
682
431
|
for (const e of entries) {
|
|
683
432
|
if (this.seen.has(e.id))
|
|
684
|
-
continue;
|
|
433
|
+
continue;
|
|
685
434
|
this.seen.add(e.id);
|
|
686
435
|
this.staged.push(e);
|
|
687
436
|
}
|
|
688
437
|
}
|
|
689
|
-
/** The staged entries, oldest-first (readStagedEntries reads these). A snapshot copy (callers don't mutate). */
|
|
690
438
|
snapshotEntries() {
|
|
691
439
|
return [...this.staged];
|
|
692
440
|
}
|
|
693
|
-
/** Commit = in-commit re-classify (dst current ids vs staged) → refuse an unconsented fork/stale → validate + the
|
|
694
|
-
* idempotent replaceEntries (validate-first-then-delete-then-import, effectively atomic locally). */
|
|
695
441
|
async commit(owner, opts) {
|
|
696
442
|
const dstEntries = await this.store.exportEntries(this.realSessionId);
|
|
697
443
|
const rel = classifySyncRelationshipByIds(this.staged.map((e) => e.id), dstEntries === null ? null : dstEntries.map((e) => e.id));
|
|
698
444
|
if ((rel.relation === "fork" || rel.relation === "stale") && opts?.resolution !== "overwrite-dst") {
|
|
699
445
|
throw new SyncConflictError(this.realSessionId, rel);
|
|
700
446
|
}
|
|
701
|
-
// 🔴 [2557] A5/F2(2026-08-05):fast_forward 的**载荷**盲区 —— dst ⊆ src 按 id,但共享 id 的
|
|
702
|
-
// 内容可能已分叉(目的端纠正过某条、源端拿旧版+追加)。整段换装会静默销毁目的端的纠正,
|
|
703
|
-
// §7 在载荷层同样要求 409(除非显式 overwrite-dst)。判据与铸形见 session-sync-content.ts。
|
|
704
447
|
if (rel.relation === "fast_forward" && opts?.resolution !== "overwrite-dst" && dstEntries !== null && dstEntries.length > 0) {
|
|
705
448
|
const dstSet = new Set(dstEntries.map((e) => e.id));
|
|
706
449
|
const diverged = fastForwardSharedContentDiverged(this.staged.filter((e) => dstSet.has(e.id)), dstEntries);
|
|
707
450
|
if (diverged)
|
|
708
451
|
throw new SyncConflictError(this.realSessionId, contentForkRelation(diverged, rel.newEntryIds));
|
|
709
452
|
}
|
|
710
|
-
// 🔴 2026-07-26:`identical` 不再等于"内容相同"。id 是 uuidv7、不是内容寻址 ⇒ 同 id 异载荷完全可能,
|
|
711
|
-
// 而 commit 这里**手上同时有两份日志**,所以内容比较不需要调用方配合(见 `session-sync-content.ts` 顶注:
|
|
712
|
-
// 我在黑板 [1701] 说过"server 单方面修不了",那句只对 Phase A 成立,对这一半是错的)。
|
|
713
|
-
// 判据不符 ⇒ **照常改写**(不抛错、不 409 —— core 定的口径是"不符只意味着多干一趟活")。
|
|
714
453
|
const contentEqual = rel.relation === "identical" && identicalIdsAlsoIdenticalContent(this.staged, dstEntries);
|
|
715
454
|
if (rel.relation !== "identical" || !contentEqual) {
|
|
716
|
-
await this.store.replaceEntries(this.realSessionId, owner, this.staged);
|
|
455
|
+
await this.store.replaceEntries(this.realSessionId, owner, this.staged);
|
|
717
456
|
}
|
|
718
457
|
else {
|
|
719
|
-
this.store.restampOwner(this.realSessionId, owner);
|
|
458
|
+
this.store.restampOwner(this.realSessionId, owner);
|
|
720
459
|
}
|
|
721
460
|
this.store.forgetStaging(this.stagingId);
|
|
722
461
|
return { relation: rel.relation };
|
|
723
462
|
}
|
|
724
|
-
/** Abort = drop the in-memory staged buffer (no durable rows were ever written locally) + forget the handle. */
|
|
725
463
|
async abort() {
|
|
726
464
|
this.staged.length = 0;
|
|
727
465
|
this.seen.clear();
|