elepha 0.1.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/LICENSE.md +375 -0
- package/README.md +135 -0
- package/bin/elepha.js +2 -0
- package/dist/adapters/base.js +600 -0
- package/dist/adapters/base.js.map +1 -0
- package/dist/adapters/claude-code.js +260 -0
- package/dist/adapters/claude-code.js.map +1 -0
- package/dist/adapters/codex.js +499 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/discriminators.js +39 -0
- package/dist/adapters/discriminators.js.map +1 -0
- package/dist/adapters/index.js +6 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/internal-command.js +3 -0
- package/dist/adapters/internal-command.js.map +1 -0
- package/dist/cli/backup-wizard.js +97 -0
- package/dist/cli/backup-wizard.js.map +1 -0
- package/dist/cli/capture-service.js +80 -0
- package/dist/cli/capture-service.js.map +1 -0
- package/dist/cli/commands/backfills.js +336 -0
- package/dist/cli/commands/backfills.js.map +1 -0
- package/dist/cli/commands/backup.js +234 -0
- package/dist/cli/commands/backup.js.map +1 -0
- package/dist/cli/commands/config.js +66 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/consent.js +237 -0
- package/dist/cli/commands/consent.js.map +1 -0
- package/dist/cli/commands/daemon-control.js +76 -0
- package/dist/cli/commands/daemon-control.js.map +1 -0
- package/dist/cli/commands/doctor.js +31 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/hook.js +24 -0
- package/dist/cli/commands/hook.js.map +1 -0
- package/dist/cli/commands/import.js +611 -0
- package/dist/cli/commands/import.js.map +1 -0
- package/dist/cli/commands/init.js +19 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/inspect.js +54 -0
- package/dist/cli/commands/inspect.js.map +1 -0
- package/dist/cli/commands/install.js +49 -0
- package/dist/cli/commands/install.js.map +1 -0
- package/dist/cli/commands/internal.js +16 -0
- package/dist/cli/commands/internal.js.map +1 -0
- package/dist/cli/commands/mcp.js +12 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/projects.js +26 -0
- package/dist/cli/commands/projects.js.map +1 -0
- package/dist/cli/commands/purge.js +216 -0
- package/dist/cli/commands/purge.js.map +1 -0
- package/dist/cli/commands/reingest.js +80 -0
- package/dist/cli/commands/reingest.js.map +1 -0
- package/dist/cli/commands/rekey.js +58 -0
- package/dist/cli/commands/rekey.js.map +1 -0
- package/dist/cli/commands/restore.js +376 -0
- package/dist/cli/commands/restore.js.map +1 -0
- package/dist/cli/commands/rollup.js +130 -0
- package/dist/cli/commands/rollup.js.map +1 -0
- package/dist/cli/commands/sanitize.js +72 -0
- package/dist/cli/commands/sanitize.js.map +1 -0
- package/dist/cli/commands/segment.js +153 -0
- package/dist/cli/commands/segment.js.map +1 -0
- package/dist/cli/commands/self-update.js +26 -0
- package/dist/cli/commands/self-update.js.map +1 -0
- package/dist/cli/commands/start.js +53 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/stats.js +42 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/commands/status.js +73 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/uninstall.js +23 -0
- package/dist/cli/commands/uninstall.js.map +1 -0
- package/dist/cli/config-wizard.js +104 -0
- package/dist/cli/config-wizard.js.map +1 -0
- package/dist/cli/destructive-op.js +32 -0
- package/dist/cli/destructive-op.js.map +1 -0
- package/dist/cli/import-wizard.js +89 -0
- package/dist/cli/import-wizard.js.map +1 -0
- package/dist/cli/index.js +74 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init-wizard.js +80 -0
- package/dist/cli/init-wizard.js.map +1 -0
- package/dist/cli/init.js +159 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/launcher-probe.js +71 -0
- package/dist/cli/launcher-probe.js.map +1 -0
- package/dist/cli/project-path.js +16 -0
- package/dist/cli/project-path.js.map +1 -0
- package/dist/cli/purge-wizard.js +200 -0
- package/dist/cli/purge-wizard.js.map +1 -0
- package/dist/cli/restore-wizard.js +101 -0
- package/dist/cli/restore-wizard.js.map +1 -0
- package/dist/cli/shared.js +128 -0
- package/dist/cli/shared.js.map +1 -0
- package/dist/cli/status.js +22 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/wordmark.js +15 -0
- package/dist/cli/wordmark.js.map +1 -0
- package/dist/config/constants.js +115 -0
- package/dist/config/constants.js.map +1 -0
- package/dist/config/env.js +168 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/memory-config.js +63 -0
- package/dist/config/memory-config.js.map +1 -0
- package/dist/config/paths.js +343 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/settings.js +132 -0
- package/dist/config/settings.js.map +1 -0
- package/dist/config/version.js +12 -0
- package/dist/config/version.js.map +1 -0
- package/dist/daemon/failure-window.js +38 -0
- package/dist/daemon/failure-window.js.map +1 -0
- package/dist/daemon/heartbeat.js +40 -0
- package/dist/daemon/heartbeat.js.map +1 -0
- package/dist/daemon/index.js +873 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/log-rotation.js +27 -0
- package/dist/daemon/log-rotation.js.map +1 -0
- package/dist/daemon/readability-guard.js +89 -0
- package/dist/daemon/readability-guard.js.map +1 -0
- package/dist/daemon/rollup-service.js +239 -0
- package/dist/daemon/rollup-service.js.map +1 -0
- package/dist/daemon/update-check.js +87 -0
- package/dist/daemon/update-check.js.map +1 -0
- package/dist/daemon/work-queue.js +28 -0
- package/dist/daemon/work-queue.js.map +1 -0
- package/dist/discovery/session-projects.js +242 -0
- package/dist/discovery/session-projects.js.map +1 -0
- package/dist/hooks/codex-trust.js +80 -0
- package/dist/hooks/codex-trust.js.map +1 -0
- package/dist/hooks/common.js +81 -0
- package/dist/hooks/common.js.map +1 -0
- package/dist/hooks/hook-log.js +38 -0
- package/dist/hooks/hook-log.js.map +1 -0
- package/dist/hooks/installer.js +200 -0
- package/dist/hooks/installer.js.map +1 -0
- package/dist/hooks/session-start.js +321 -0
- package/dist/hooks/session-start.js.map +1 -0
- package/dist/hooks/user-prompt-submit.js +248 -0
- package/dist/hooks/user-prompt-submit.js.map +1 -0
- package/dist/install/binary.js +72 -0
- package/dist/install/binary.js.map +1 -0
- package/dist/install/config-file.js +119 -0
- package/dist/install/config-file.js.map +1 -0
- package/dist/install/daemon-health.js +48 -0
- package/dist/install/daemon-health.js.map +1 -0
- package/dist/install/daemon-service.js +309 -0
- package/dist/install/daemon-service.js.map +1 -0
- package/dist/install/doctor.js +180 -0
- package/dist/install/doctor.js.map +1 -0
- package/dist/install/health-checks.js +90 -0
- package/dist/install/health-checks.js.map +1 -0
- package/dist/install/installer.js +411 -0
- package/dist/install/installer.js.map +1 -0
- package/dist/install/launchd-backend.js +261 -0
- package/dist/install/launchd-backend.js.map +1 -0
- package/dist/install/launcher.js +224 -0
- package/dist/install/launcher.js.map +1 -0
- package/dist/install/markers.js +14 -0
- package/dist/install/markers.js.map +1 -0
- package/dist/install/platform.js +29 -0
- package/dist/install/platform.js.map +1 -0
- package/dist/install/present-tools.js +16 -0
- package/dist/install/present-tools.js.map +1 -0
- package/dist/install/self-update.js +120 -0
- package/dist/install/self-update.js.map +1 -0
- package/dist/install/service-backend.js +36 -0
- package/dist/install/service-backend.js.map +1 -0
- package/dist/install/service-manifest.js +36 -0
- package/dist/install/service-manifest.js.map +1 -0
- package/dist/install/status.js +58 -0
- package/dist/install/status.js.map +1 -0
- package/dist/install/systemd-backend.js +225 -0
- package/dist/install/systemd-backend.js.map +1 -0
- package/dist/markers.js +5 -0
- package/dist/markers.js.map +1 -0
- package/dist/mcp/installer.js +131 -0
- package/dist/mcp/installer.js.map +1 -0
- package/dist/mcp/server.js +81 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools/get-context.js +3 -0
- package/dist/mcp/tools/get-context.js.map +1 -0
- package/dist/mcp/tools/list-projects.js +3 -0
- package/dist/mcp/tools/list-projects.js.map +1 -0
- package/dist/mcp/tools/save-memory.js +3 -0
- package/dist/mcp/tools/save-memory.js.map +1 -0
- package/dist/mcp/tools.js +288 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/rendering/raw-turn-renderer.js +85 -0
- package/dist/rendering/raw-turn-renderer.js.map +1 -0
- package/dist/security/file-permissions.js +15 -0
- package/dist/security/file-permissions.js.map +1 -0
- package/dist/security/provider-transcript.js +74 -0
- package/dist/security/provider-transcript.js.map +1 -0
- package/dist/security/sanitize.js +210 -0
- package/dist/security/sanitize.js.map +1 -0
- package/dist/security/self-ingestion.js +47 -0
- package/dist/security/self-ingestion.js.map +1 -0
- package/dist/security/sentinel.js +19 -0
- package/dist/security/sentinel.js.map +1 -0
- package/dist/security/subprocess-allowlist.js +421 -0
- package/dist/security/subprocess-allowlist.js.map +1 -0
- package/dist/serving/instructions.js +33 -0
- package/dist/serving/instructions.js.map +1 -0
- package/dist/serving/lexical-recall.js +249 -0
- package/dist/serving/lexical-recall.js.map +1 -0
- package/dist/serving/session-reader.js +302 -0
- package/dist/serving/session-reader.js.map +1 -0
- package/dist/storage/backfill-runner.js +34 -0
- package/dist/storage/backfill-runner.js.map +1 -0
- package/dist/storage/backup.js +46 -0
- package/dist/storage/backup.js.map +1 -0
- package/dist/storage/candidate-validator.js +77 -0
- package/dist/storage/candidate-validator.js.map +1 -0
- package/dist/storage/consent-store.js +198 -0
- package/dist/storage/consent-store.js.map +1 -0
- package/dist/storage/custom-title-backfill.js +68 -0
- package/dist/storage/custom-title-backfill.js.map +1 -0
- package/dist/storage/db.js +312 -0
- package/dist/storage/db.js.map +1 -0
- package/dist/storage/external-agent-import-purge.js +182 -0
- package/dist/storage/external-agent-import-purge.js.map +1 -0
- package/dist/storage/first-prompt-search-backfill.js +76 -0
- package/dist/storage/first-prompt-search-backfill.js.map +1 -0
- package/dist/storage/first-prompt-search.js +7 -0
- package/dist/storage/first-prompt-search.js.map +1 -0
- package/dist/storage/injection-store.js +37 -0
- package/dist/storage/injection-store.js.map +1 -0
- package/dist/storage/memory-store.js +428 -0
- package/dist/storage/memory-store.js.map +1 -0
- package/dist/storage/project-resolver.js +294 -0
- package/dist/storage/project-resolver.js.map +1 -0
- package/dist/storage/project-store.js +104 -0
- package/dist/storage/project-store.js.map +1 -0
- package/dist/storage/rendered-chars-backfill.js +85 -0
- package/dist/storage/rendered-chars-backfill.js.map +1 -0
- package/dist/storage/resegmentation.js +582 -0
- package/dist/storage/resegmentation.js.map +1 -0
- package/dist/storage/rollup-store.js +246 -0
- package/dist/storage/rollup-store.js.map +1 -0
- package/dist/storage/root-commit-backfill.js +32 -0
- package/dist/storage/root-commit-backfill.js.map +1 -0
- package/dist/storage/sanitize-backfill.js +184 -0
- package/dist/storage/sanitize-backfill.js.map +1 -0
- package/dist/storage/segmentation.js +87 -0
- package/dist/storage/segmentation.js.map +1 -0
- package/dist/storage/session-fields-backfill.js +196 -0
- package/dist/storage/session-fields-backfill.js.map +1 -0
- package/dist/storage/session-read-model.js +130 -0
- package/dist/storage/session-read-model.js.map +1 -0
- package/dist/storage/session-store.js +200 -0
- package/dist/storage/session-store.js.map +1 -0
- package/dist/storage/session-title-backfill.js +75 -0
- package/dist/storage/session-title-backfill.js.map +1 -0
- package/dist/storage/session-title.js +71 -0
- package/dist/storage/session-title.js.map +1 -0
- package/dist/storage/shown-session-list-store.js +33 -0
- package/dist/storage/shown-session-list-store.js.map +1 -0
- package/dist/storage/stats.js +30 -0
- package/dist/storage/stats.js.map +1 -0
- package/dist/storage/turn-store.js +202 -0
- package/dist/storage/turn-store.js.map +1 -0
- package/dist/storage/ulid.js +17 -0
- package/dist/storage/ulid.js.map +1 -0
- package/dist/summarizer/anthropic-call.js +76 -0
- package/dist/summarizer/anthropic-call.js.map +1 -0
- package/dist/summarizer/call-log.js +120 -0
- package/dist/summarizer/call-log.js.map +1 -0
- package/dist/summarizer/haiku-provider.js +136 -0
- package/dist/summarizer/haiku-provider.js.map +1 -0
- package/dist/summarizer/pricing.js +8 -0
- package/dist/summarizer/pricing.js.map +1 -0
- package/dist/summarizer/prompt-cache.js +6 -0
- package/dist/summarizer/prompt-cache.js.map +1 -0
- package/dist/summarizer/prompt.js +47 -0
- package/dist/summarizer/prompt.js.map +1 -0
- package/dist/summarizer/provider-config.js +25 -0
- package/dist/summarizer/provider-config.js.map +1 -0
- package/dist/summarizer/rollup-prompt.js +225 -0
- package/dist/summarizer/rollup-prompt.js.map +1 -0
- package/dist/summarizer/rollup-provider.js +224 -0
- package/dist/summarizer/rollup-provider.js.map +1 -0
- package/dist/types/index.js +10 -0
- package/dist/types/index.js.map +1 -0
- package/dist/util/error.js +5 -0
- package/dist/util/error.js.map +1 -0
- package/dist/util/fs.js +97 -0
- package/dist/util/fs.js.map +1 -0
- package/dist/util/relative-time.js +12 -0
- package/dist/util/relative-time.js.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Persistence for session-level rollups.
|
|
2
|
+
//
|
|
3
|
+
// The hard problem here is idempotency. `memories` is protected by
|
|
4
|
+
// UNIQUE(session_id, turn_index): re-processing a turn is a no-op. A rollup is
|
|
5
|
+
// a MUTABLE AGGREGATE with no such guard - applying the same new turns twice
|
|
6
|
+
// would double-count decisions, resurrect resolved pending_items, and inflate
|
|
7
|
+
// turn_count. And re-processing is not hypothetical: "closed" is a heuristic
|
|
8
|
+
// (file idle), and Codex Desktop reopens sessions days later and appends to the
|
|
9
|
+
// original rollout, so final -> live -> final can repeat indefinitely.
|
|
10
|
+
//
|
|
11
|
+
// Two independent guards, either of which alone would be sufficient:
|
|
12
|
+
//
|
|
13
|
+
// 1. WATERMARK, enforced in SQL. rolled_up_through_turn_index advances in the
|
|
14
|
+
// SAME transaction that writes the content it accounts for, and the write
|
|
15
|
+
// is conditional on the watermark still being where the caller read it
|
|
16
|
+
// (`WHERE rolled_up_through_turn_index = :expected`). A duplicate or
|
|
17
|
+
// concurrent apply finds the watermark moved and writes nothing. A crash
|
|
18
|
+
// between the LLM call and the commit costs one wasted call and changes
|
|
19
|
+
// nothing on disk.
|
|
20
|
+
// 2. SET SEMANTICS in the merge itself. decisions dedupe by `what`,
|
|
21
|
+
// pending_items and files_touched by value. Applying the same merge twice
|
|
22
|
+
// yields the same arrays.
|
|
23
|
+
import { dedupePaths } from '../config/paths.js';
|
|
24
|
+
import { escapeShellSyntax, stripShellSyntax } from '../security/sanitize.js';
|
|
25
|
+
/**
|
|
26
|
+
* Every rollup from the old schema is stale, for three independent reasons
|
|
27
|
+
* that all landed together so the corpus is rebuilt once rather than three
|
|
28
|
+
* times:
|
|
29
|
+
*
|
|
30
|
+
* - the prompt builder end-truncated its input, so on large sessions the
|
|
31
|
+
* newest turns never reached the model (measured: 83% of all lost decisions);
|
|
32
|
+
* - decisions now carry `turnIndex`/`at` provenance, assigned in code;
|
|
33
|
+
* - `why` is now captured at the turn, where the transcript is still in the
|
|
34
|
+
* prompt, instead of being inferred afterwards from the decision text.
|
|
35
|
+
*
|
|
36
|
+
* `elepha rollup --rebuild` is what actually reaches them: the default sweep
|
|
37
|
+
* skips `final` rollups.
|
|
38
|
+
*/
|
|
39
|
+
export const ROLLUP_VERSION = 2;
|
|
40
|
+
/**
|
|
41
|
+
* Newest-first by provenance. Decisions with no `turnIndex` (pre-provenance
|
|
42
|
+
* rows) sort oldest, which is both true and the safe direction: they are never
|
|
43
|
+
* preferred over a decision we can actually place in time.
|
|
44
|
+
*/
|
|
45
|
+
export function sortDecisionsByTurn(decisions) {
|
|
46
|
+
return [...decisions].sort((a, b) => (b.turnIndex ?? -1) - (a.turnIndex ?? -1));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The newest K decisions, in turn order. This is the selection the brief and
|
|
50
|
+
* the carried-rollup budget both need, and it lives HERE, in code, rather than
|
|
51
|
+
* in a prompt instruction - the merge model demonstrably drops new decisions
|
|
52
|
+
* when asked politely not to, and a selection rule that the model can violate
|
|
53
|
+
* is not a selection rule.
|
|
54
|
+
*/
|
|
55
|
+
export function newestDecisions(decisions, k) {
|
|
56
|
+
return sortDecisionsByTurn(decisions)
|
|
57
|
+
.slice(0, k)
|
|
58
|
+
.sort((a, b) => (a.turnIndex ?? -1) - (b.turnIndex ?? -1));
|
|
59
|
+
}
|
|
60
|
+
function hydrate(row) {
|
|
61
|
+
if (!row) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
...row,
|
|
66
|
+
decisions: JSON.parse(row.decisions),
|
|
67
|
+
pending_items: JSON.parse(row.pending_items),
|
|
68
|
+
files_touched: JSON.parse(row.files_touched),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Merges two rollup halves with set semantics, so applying the same merge twice is a no-op. */
|
|
72
|
+
export function mergeRollupContent(previous, incoming) {
|
|
73
|
+
const byWhat = new Map();
|
|
74
|
+
for (const d of [...previous.decisions, ...incoming.decisions]) {
|
|
75
|
+
// Sanitize-normalized key. `previous` was read back from the store and
|
|
76
|
+
// is therefore already escaped (Rule 3 runs in write() below), while
|
|
77
|
+
// `incoming` is raw summarizer output. Keying on the raw text would
|
|
78
|
+
// make the same decision hash two different ways across the sanitize
|
|
79
|
+
// boundary and defeat the dedupe that makes this merge idempotent.
|
|
80
|
+
const key = escapeShellSyntax(d.what).trim().toLowerCase();
|
|
81
|
+
const seen = byWhat.get(key);
|
|
82
|
+
if (!seen) {
|
|
83
|
+
byWhat.set(key, d);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// Same decision seen twice: keep the EARLIEST turn_index, because the
|
|
87
|
+
// first occurrence is when the choice was actually made - a later
|
|
88
|
+
// restatement is the model repeating itself, not a new decision. But
|
|
89
|
+
// prefer any provenance over none, so a pre-provenance row does not
|
|
90
|
+
// block a placed one.
|
|
91
|
+
if (seen.turnIndex === undefined && d.turnIndex !== undefined) {
|
|
92
|
+
byWhat.set(key, { ...seen, turnIndex: d.turnIndex, at: d.at });
|
|
93
|
+
}
|
|
94
|
+
else if (seen.turnIndex !== undefined && d.turnIndex !== undefined && d.turnIndex < seen.turnIndex) {
|
|
95
|
+
byWhat.set(key, { ...seen, turnIndex: d.turnIndex, at: d.at });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// pending_items come from the merge model, which is told to drop resolved
|
|
99
|
+
// ones - so incoming REPLACES rather than unions, otherwise a resolved item
|
|
100
|
+
// could never leave the list.
|
|
101
|
+
const pendingItems = [...new Set(incoming.pendingItems.map((p) => p.trim()).filter(Boolean))];
|
|
102
|
+
return {
|
|
103
|
+
// Stored in turn order, oldest first, so array position carries meaning
|
|
104
|
+
// and "the newest K" is a slice rather than a search.
|
|
105
|
+
decisions: [...byWhat.values()].sort((a, b) => (a.turnIndex ?? -1) - (b.turnIndex ?? -1)),
|
|
106
|
+
pendingItems,
|
|
107
|
+
filesTouched: dedupePaths([...previous.filesTouched, ...incoming.filesTouched]),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export class RollupStore {
|
|
111
|
+
db;
|
|
112
|
+
stmts;
|
|
113
|
+
constructor(db) {
|
|
114
|
+
this.db = db;
|
|
115
|
+
this.stmts = {
|
|
116
|
+
get: db.prepare('SELECT * FROM session_rollups WHERE session_id = ?'),
|
|
117
|
+
// Upsert, not a plain insert: this path serves both the first
|
|
118
|
+
// rollup and a rollup_version rebuild, where a row already exists
|
|
119
|
+
// and must be replaced wholesale rather than merged onto.
|
|
120
|
+
insert: db.prepare(`INSERT INTO session_rollups
|
|
121
|
+
(session_id, project_id, tool, title, summary, decisions, pending_items, files_touched,
|
|
122
|
+
turn_count, started_at, ended_at, kind, parent_session_id, summarizer_status,
|
|
123
|
+
rollup_state, rolled_up_through_turn_index, computed_at, rollup_version)
|
|
124
|
+
VALUES (@session_id, @project_id, @tool, @title, @summary, @decisions, @pending_items, @files_touched,
|
|
125
|
+
@turn_count, @started_at, @ended_at, @kind, @parent_session_id, @summarizer_status,
|
|
126
|
+
@rollup_state, @through, @now, @version)
|
|
127
|
+
ON CONFLICT (session_id) DO UPDATE SET
|
|
128
|
+
title = excluded.title, summary = excluded.summary, decisions = excluded.decisions,
|
|
129
|
+
pending_items = excluded.pending_items, files_touched = excluded.files_touched,
|
|
130
|
+
turn_count = excluded.turn_count, started_at = excluded.started_at, ended_at = excluded.ended_at,
|
|
131
|
+
kind = excluded.kind, parent_session_id = excluded.parent_session_id,
|
|
132
|
+
summarizer_status = excluded.summarizer_status,
|
|
133
|
+
rollup_state = excluded.rollup_state,
|
|
134
|
+
rolled_up_through_turn_index = excluded.rolled_up_through_turn_index,
|
|
135
|
+
computed_at = excluded.computed_at, rollup_version = excluded.rollup_version`),
|
|
136
|
+
// Conditional on the watermark the caller observed: a second apply
|
|
137
|
+
// of the same turns finds it already advanced and changes nothing.
|
|
138
|
+
updateIfWatermark: db.prepare(`UPDATE session_rollups SET
|
|
139
|
+
title = @title, summary = @summary, decisions = @decisions,
|
|
140
|
+
pending_items = @pending_items, files_touched = @files_touched,
|
|
141
|
+
turn_count = @turn_count, ended_at = @ended_at,
|
|
142
|
+
summarizer_status = @summarizer_status,
|
|
143
|
+
rollup_state = @rollup_state, rolled_up_through_turn_index = @through,
|
|
144
|
+
computed_at = @now, rollup_version = @version
|
|
145
|
+
WHERE session_id = @session_id AND rolled_up_through_turn_index = @expected`),
|
|
146
|
+
markLive: db.prepare(`UPDATE session_rollups SET rollup_state = 'live' WHERE session_id = ? AND rollup_state = 'final'`),
|
|
147
|
+
listByProject: db.prepare('SELECT * FROM session_rollups WHERE project_id = ? ORDER BY ended_at DESC'),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
get(sessionId) {
|
|
151
|
+
return hydrate(this.stmts.get.get(sessionId));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Writes a rollup. `expectedThroughTurnIndex` is the watermark the caller
|
|
155
|
+
* read before computing; the update only lands if it hasn't moved since.
|
|
156
|
+
* Returns false when the write was rejected as already-applied - that is a
|
|
157
|
+
* normal outcome, not an error.
|
|
158
|
+
*/
|
|
159
|
+
write(w, expectedThroughTurnIndex) {
|
|
160
|
+
// Security Rule 3 CHOKE POINT, not caller discipline: a caller can
|
|
161
|
+
// forget, a store cannot. Display strings are stripped (the
|
|
162
|
+
// metacharacter carries nothing there); decisions are escaped, because
|
|
163
|
+
// a decision may legitimately need to name the syntax it ruled out.
|
|
164
|
+
const params = {
|
|
165
|
+
session_id: w.sessionId,
|
|
166
|
+
project_id: w.projectId,
|
|
167
|
+
tool: w.tool,
|
|
168
|
+
title: stripShellSyntax(w.title),
|
|
169
|
+
summary: stripShellSyntax(w.summary),
|
|
170
|
+
decisions: JSON.stringify(w.decisions.map((d) => ({
|
|
171
|
+
what: escapeShellSyntax(d.what),
|
|
172
|
+
why: escapeShellSyntax(d.why),
|
|
173
|
+
...(d.turnIndex !== undefined ? { turnIndex: d.turnIndex } : {}),
|
|
174
|
+
...(d.at !== undefined ? { at: d.at } : {}),
|
|
175
|
+
}))),
|
|
176
|
+
pending_items: JSON.stringify(w.pendingItems.map(stripShellSyntax)),
|
|
177
|
+
files_touched: JSON.stringify(dedupePaths(w.filesTouched)),
|
|
178
|
+
turn_count: w.turnCount,
|
|
179
|
+
started_at: w.startedAt,
|
|
180
|
+
ended_at: w.endedAt,
|
|
181
|
+
kind: w.kind,
|
|
182
|
+
parent_session_id: w.parentSessionId,
|
|
183
|
+
summarizer_status: w.summarizerStatus,
|
|
184
|
+
rollup_state: w.state,
|
|
185
|
+
through: w.throughTurnIndex,
|
|
186
|
+
now: new Date().toISOString(),
|
|
187
|
+
version: w.rollupVersion ?? ROLLUP_VERSION,
|
|
188
|
+
};
|
|
189
|
+
const run = this.db.transaction(() => {
|
|
190
|
+
if (expectedThroughTurnIndex === undefined) {
|
|
191
|
+
this.stmts.insert.run(params);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
return this.stmts.updateIfWatermark.run({ ...params, expected: expectedThroughTurnIndex }).changes > 0;
|
|
195
|
+
});
|
|
196
|
+
return run();
|
|
197
|
+
}
|
|
198
|
+
/** Reopened session: a final rollup returns to live so the next close recomputes it incrementally. */
|
|
199
|
+
markLive(sessionId) {
|
|
200
|
+
this.stmts.markLive.run(sessionId);
|
|
201
|
+
}
|
|
202
|
+
listByProject(projectId) {
|
|
203
|
+
return this.stmts.listByProject.all(projectId).map((r) => hydrate(r));
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The listing view: top-level sessions with their sub-agent work attached,
|
|
207
|
+
* rather than sub-agents competing for space as peers. Sub-agents are 36% of
|
|
208
|
+
* Claude Code sessions locally (16 of 44) at a median of one turn each, so
|
|
209
|
+
* listing them flat buries the real sessions.
|
|
210
|
+
*
|
|
211
|
+
* Nothing is deleted from storage, and every child stays reachable through
|
|
212
|
+
* its parent. The served read model computes substance, so this
|
|
213
|
+
* legacy rollup listing no longer reads the inert stored column.
|
|
214
|
+
*/
|
|
215
|
+
listSessions(projectId, options = {}) {
|
|
216
|
+
const all = this.listByProject(projectId);
|
|
217
|
+
const childrenByParent = new Map();
|
|
218
|
+
for (const r of all) {
|
|
219
|
+
if (r.parent_session_id == null) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const bucket = childrenByParent.get(r.parent_session_id);
|
|
223
|
+
if (bucket) {
|
|
224
|
+
bucket.push(r);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
childrenByParent.set(r.parent_session_id, [r]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const entries = [];
|
|
231
|
+
for (const rollup of all) {
|
|
232
|
+
const isChild = rollup.parent_session_id != null;
|
|
233
|
+
// A child whose parent is present in this project is surfaced under
|
|
234
|
+
// the parent. An orphaned child (parent never ingested, or in
|
|
235
|
+
// another project) is still listed on its own - hiding it would
|
|
236
|
+
// lose it entirely.
|
|
237
|
+
const parentPresent = isChild && all.some((r) => r.session_id === rollup.parent_session_id);
|
|
238
|
+
if (parentPresent && !options.includeAll) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
entries.push({ rollup, children: childrenByParent.get(rollup.session_id) ?? [] });
|
|
242
|
+
}
|
|
243
|
+
return entries;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=rollup-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rollup-store.js","sourceRoot":"","sources":["../../src/storage/rollup-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Reports legacy project rows whose current repository identity can be recovered without writing. */
|
|
2
|
+
export function planRootCommitBackfill(db, resolveGitRoot, resolveGitRootCommit) {
|
|
3
|
+
const projects = db.prepare('SELECT id, path, git_root FROM projects WHERE git_root_commit IS NULL ORDER BY id').all();
|
|
4
|
+
const changes = [];
|
|
5
|
+
let projectsUnresolvable = 0;
|
|
6
|
+
for (const project of projects) {
|
|
7
|
+
const gitRoot = project.git_root || resolveGitRoot(project.path);
|
|
8
|
+
const gitRootCommit = gitRoot ? resolveGitRootCommit(gitRoot) : null;
|
|
9
|
+
if (gitRootCommit === null) {
|
|
10
|
+
projectsUnresolvable++;
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
changes.push({ projectId: project.id, path: project.path, gitRootCommit });
|
|
14
|
+
}
|
|
15
|
+
return { changes, projectsScanned: projects.length, projectsUnresolvable, projectsSkippedConcurrent: 0 };
|
|
16
|
+
}
|
|
17
|
+
/** Fills only project rows that still need the identity captured by the fresh plan. */
|
|
18
|
+
export function applyRootCommitBackfill(db, resolveGitRoot, resolveGitRootCommit) {
|
|
19
|
+
const plan = planRootCommitBackfill(db, resolveGitRoot, resolveGitRootCommit);
|
|
20
|
+
const apply = db.transaction((changes) => {
|
|
21
|
+
let projectsSkippedConcurrent = 0;
|
|
22
|
+
const update = db.prepare('UPDATE projects SET git_root_commit = ? WHERE id = ? AND git_root_commit IS NULL');
|
|
23
|
+
for (const change of changes) {
|
|
24
|
+
if (update.run(change.gitRootCommit, change.projectId).changes === 0) {
|
|
25
|
+
projectsSkippedConcurrent++;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return projectsSkippedConcurrent;
|
|
29
|
+
});
|
|
30
|
+
return { ...plan, projectsSkippedConcurrent: apply(plan.changes) };
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=root-commit-backfill.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-commit-backfill.js","sourceRoot":"","sources":["../../src/storage/root-commit-backfill.ts"],"names":[],"mappings":"AAwBA,sGAAsG;AACtG,MAAM,UAAU,sBAAsB,CAClC,EAAY,EACZ,cAA8B,EAC9B,oBAA0C;IAE1C,MAAM,QAAQ,GAAG,EAAE,CAAC,OAAO,CAAC,mFAAmF,CAAC,CAAC,GAAG,EAAmB,CAAC;IACxI,MAAM,OAAO,GAA+B,EAAE,CAAC;IAC/C,IAAI,oBAAoB,GAAG,CAAC,CAAC;IAE7B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,IAAI,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACjE,MAAM,aAAa,GAAG,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACrE,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;YACzB,oBAAoB,EAAE,CAAC;YACvB,SAAS;QACb,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC;IAC/E,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,QAAQ,CAAC,MAAM,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,CAAC,EAAE,CAAC;AAC7G,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,uBAAuB,CACnC,EAAY,EACZ,cAA8B,EAC9B,oBAA0C;IAE1C,MAAM,IAAI,GAAG,sBAAsB,CAAC,EAAE,EAAE,cAAc,EAAE,oBAAoB,CAAC,CAAC;IAC9E,MAAM,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,CAAC,OAAmC,EAAE,EAAE;QACjE,IAAI,yBAAyB,GAAG,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CAAC,kFAAkF,CAAC,CAAC;QAC9G,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC3B,IAAI,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,KAAK,CAAC,EAAE,CAAC;gBACnE,yBAAyB,EAAE,CAAC;YAChC,CAAC;QACL,CAAC;QACD,OAAO,yBAAyB,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,OAAO,EAAE,GAAG,IAAI,EAAE,yBAAyB,EAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;AACvE,CAAC"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Security Rule 3 backfill. The choke points in RollupStore.write() and
|
|
2
|
+
// MemoryStore.recordTurn/reingestTurn() clean everything written from now on;
|
|
3
|
+
// this cleans what the store already holds.
|
|
4
|
+
//
|
|
5
|
+
// Rule 3 debt is real, not hypothetical - the live corpus has rollup rows and
|
|
6
|
+
// memory rows carrying backticks and `$(`. Benign today (markdown-ish quoting
|
|
7
|
+
// of config keys), which is exactly the point: the store holds shell-active
|
|
8
|
+
// syntax and until now nothing checked it.
|
|
9
|
+
//
|
|
10
|
+
// Destructive-operation shape: preview the affected rows with
|
|
11
|
+
// before/after text (the list, not just a count - aggregates hide
|
|
12
|
+
// misclassification), back up, run in one transaction, then re-run the
|
|
13
|
+
// detector store-wide and fail loudly if anything survives.
|
|
14
|
+
//
|
|
15
|
+
// files_touched is deliberately NOT sanitized. Those paths are computed
|
|
16
|
+
// deterministically from tool-call arguments, not emitted by the summarizer,
|
|
17
|
+
// and escaping a metacharacter that is genuinely part of a filename would
|
|
18
|
+
// corrupt the path rather than protect anything. A pathological filename is
|
|
19
|
+
// caught instead by the read-time assertion when it reaches an assembled
|
|
20
|
+
// brief.
|
|
21
|
+
import { detectShellSyntax, escapeShellSyntax, stripShellSyntax } from '../security/sanitize.js';
|
|
22
|
+
function mapJsonField(raw, mapper) {
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(raw);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// A field that does not parse cannot be field-mapped, but it is still
|
|
29
|
+
// stored text served to a model - sanitize the whole blob as a display
|
|
30
|
+
// string rather than leaving it alone. Silently skipping is the
|
|
31
|
+
// failure mode this codebase already paid for.
|
|
32
|
+
return stripShellSyntax(raw);
|
|
33
|
+
}
|
|
34
|
+
return JSON.stringify(mapper(parsed));
|
|
35
|
+
}
|
|
36
|
+
const sanitizeDecisionsJson = (parsed) => {
|
|
37
|
+
if (!Array.isArray(parsed)) {
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
// memories holds string[]; session_rollups holds {what, why}[]. Both are
|
|
41
|
+
// the escape policy - a decision may need to name the syntax it ruled out.
|
|
42
|
+
return parsed.map((d) => {
|
|
43
|
+
if (typeof d === 'string') {
|
|
44
|
+
return escapeShellSyntax(d);
|
|
45
|
+
}
|
|
46
|
+
if (d && typeof d === 'object' && 'what' in d) {
|
|
47
|
+
const rec = d;
|
|
48
|
+
return {
|
|
49
|
+
...d,
|
|
50
|
+
what: typeof rec.what === 'string' ? escapeShellSyntax(rec.what) : rec.what,
|
|
51
|
+
why: typeof rec.why === 'string' ? escapeShellSyntax(rec.why) : rec.why,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return d;
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
const sanitizeStringsJson = (parsed) => Array.isArray(parsed) ? parsed.map((s) => (typeof s === 'string' ? stripShellSyntax(s) : s)) : parsed;
|
|
58
|
+
export function sanitizeRollupDisplayField(raw) {
|
|
59
|
+
return stripShellSyntax(raw);
|
|
60
|
+
}
|
|
61
|
+
export function sanitizeRollupDecisionsField(raw) {
|
|
62
|
+
return mapJsonField(raw, sanitizeDecisionsJson);
|
|
63
|
+
}
|
|
64
|
+
export function sanitizeRollupPendingItemsField(raw) {
|
|
65
|
+
return mapJsonField(raw, sanitizeStringsJson);
|
|
66
|
+
}
|
|
67
|
+
const ROLLUP_FIELDS = [
|
|
68
|
+
{ field: 'title', json: false, transform: sanitizeRollupDisplayField },
|
|
69
|
+
{ field: 'summary', json: false, transform: sanitizeRollupDisplayField },
|
|
70
|
+
{ field: 'decisions', json: true, transform: sanitizeRollupDecisionsField },
|
|
71
|
+
{ field: 'pending_items', json: true, transform: sanitizeRollupPendingItemsField },
|
|
72
|
+
];
|
|
73
|
+
const MEMORY_FIELDS = [
|
|
74
|
+
{ field: 'decisions', json: true, transform: (raw) => mapJsonField(raw, sanitizeDecisionsJson) },
|
|
75
|
+
{ field: 'pending_items', json: true, transform: (raw) => mapJsonField(raw, sanitizeStringsJson) },
|
|
76
|
+
];
|
|
77
|
+
function collect(db, table, idColumn, fields) {
|
|
78
|
+
const columns = fields.map((f) => f.field).join(', ');
|
|
79
|
+
const rows = db.prepare(`SELECT ${idColumn} AS __id, ${columns} FROM ${table}`).all();
|
|
80
|
+
const changes = [];
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
for (const spec of fields) {
|
|
83
|
+
const before = row[spec.field];
|
|
84
|
+
if (typeof before !== 'string') {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const after = spec.transform(before);
|
|
88
|
+
if (after !== before) {
|
|
89
|
+
changes.push({ table, rowId: Number(row.__id), field: spec.field, before, after });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return changes;
|
|
94
|
+
}
|
|
95
|
+
/** What the backfill would change, without changing anything. */
|
|
96
|
+
export function planSanitize(db) {
|
|
97
|
+
const changes = [...collect(db, 'session_rollups', 'session_id', ROLLUP_FIELDS), ...collect(db, 'memories', 'id', MEMORY_FIELDS)];
|
|
98
|
+
return {
|
|
99
|
+
changes,
|
|
100
|
+
rollupRows: new Set(changes.filter((c) => c.table === 'session_rollups').map((c) => c.rowId)).size,
|
|
101
|
+
memoryRows: new Set(changes.filter((c) => c.table === 'memories').map((c) => c.rowId)).size,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Applies planSanitize's plan in a single transaction. Returns the plan that was applied, for reporting. */
|
|
105
|
+
export function applySanitize(db) {
|
|
106
|
+
const plan = planSanitize(db);
|
|
107
|
+
const apply = db.transaction(() => {
|
|
108
|
+
for (const c of plan.changes) {
|
|
109
|
+
const idColumn = c.table === 'session_rollups' ? 'session_id' : 'id';
|
|
110
|
+
db.prepare(`UPDATE ${c.table} SET ${c.field} = ? WHERE ${idColumn} = ?`).run(c.after, c.rowId);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
apply();
|
|
114
|
+
return plan;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Post-backfill verification: re-run the detector over every sanitized field
|
|
118
|
+
* in the store. This is the check that turns "the store never holds executable
|
|
119
|
+
* syntax" from a comment into an invariant, so it re-reads from SQL rather
|
|
120
|
+
* than trusting the plan it just applied.
|
|
121
|
+
*/
|
|
122
|
+
export function verifySanitize(db) {
|
|
123
|
+
const residue = [];
|
|
124
|
+
const check = (table, idColumn, fields) => {
|
|
125
|
+
const columns = fields.map((f) => f.field).join(', ');
|
|
126
|
+
const rows = db.prepare(`SELECT ${idColumn} AS __id, ${columns} FROM ${table}`).all();
|
|
127
|
+
for (const row of rows) {
|
|
128
|
+
for (const spec of fields) {
|
|
129
|
+
const raw = row[spec.field];
|
|
130
|
+
if (typeof raw !== 'string') {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
for (const value of leafStrings(raw, spec.json)) {
|
|
134
|
+
if (detectShellSyntax(value)) {
|
|
135
|
+
residue.push({ table, rowId: Number(row.__id), field: spec.field, text: value });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
check('session_rollups', 'session_id', ROLLUP_FIELDS);
|
|
142
|
+
check('memories', 'id', MEMORY_FIELDS);
|
|
143
|
+
return residue;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The stored strings inside a field, JSON-decoded where the field is JSON.
|
|
147
|
+
*
|
|
148
|
+
* Running the detector on the raw column text instead would report false
|
|
149
|
+
* positives: JSON escapes a backslash as `\\`, so a correctly escaped
|
|
150
|
+
* `` \` `` is stored as `` \\` ``, whose backslash parity reads as
|
|
151
|
+
* "unescaped backtick" to the detector. The invariant is about the values the
|
|
152
|
+
* store hands out, not their transport encoding.
|
|
153
|
+
*/
|
|
154
|
+
function leafStrings(raw, isJson) {
|
|
155
|
+
if (!isJson) {
|
|
156
|
+
return [raw];
|
|
157
|
+
}
|
|
158
|
+
let parsed;
|
|
159
|
+
try {
|
|
160
|
+
parsed = JSON.parse(raw);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return [raw]; // unparseable JSON is still stored text; check it as-is
|
|
164
|
+
}
|
|
165
|
+
const out = [];
|
|
166
|
+
const walk = (node) => {
|
|
167
|
+
if (typeof node === 'string') {
|
|
168
|
+
out.push(node);
|
|
169
|
+
}
|
|
170
|
+
else if (Array.isArray(node)) {
|
|
171
|
+
for (const child of node) {
|
|
172
|
+
walk(child);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else if (node && typeof node === 'object') {
|
|
176
|
+
for (const child of Object.values(node)) {
|
|
177
|
+
walk(child);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
walk(parsed);
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=sanitize-backfill.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize-backfill.js","sourceRoot":"","sources":["../../src/storage/sanitize-backfill.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,8EAA8E;AAC9E,4CAA4C;AAC5C,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,4EAA4E;AAC5E,2CAA2C;AAC3C,EAAE;AACF,8DAA8D;AAC9D,kEAAkE;AAClE,uEAAuE;AACvE,4DAA4D;AAC5D,EAAE;AACF,wEAAwE;AACxE,6EAA6E;AAC7E,0EAA0E;AAC1E,4EAA4E;AAC5E,yEAAyE;AACzE,SAAS;AAGT,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAmBjG,SAAS,YAAY,CAAC,GAAW,EAAE,MAAkB;IACjD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACD,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACL,sEAAsE;QACtE,uEAAuE;QACvE,gEAAgE;QAChE,+CAA+C;QAC/C,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,qBAAqB,GAAe,CAAC,MAAM,EAAE,EAAE;IACjD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAClB,CAAC;IACD,yEAAyE;IACzE,2EAA2E;IAC3E,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACpB,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;YACxB,OAAO,iBAAiB,CAAC,CAAC,CAAC,CAAC;QAChC,CAAC;QACD,IAAI,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;YAC5C,MAAM,GAAG,GAAG,CAAoC,CAAC;YACjD,OAAO;gBACH,GAAG,CAAC;gBACJ,IAAI,EAAE,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI;gBAC3E,GAAG,EAAE,OAAO,GAAG,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG;aAC1E,CAAC;QACN,CAAC;QACD,OAAO,CAAC,CAAC;IACb,CAAC,CAAC,CAAC;AACP,CAAC,CAAC;AAEF,MAAM,mBAAmB,GAAe,CAAC,MAAM,EAAE,EAAE,CAC/C,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;AAE1G,MAAM,UAAU,0BAA0B,CAAC,GAAW;IAClD,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,4BAA4B,CAAC,GAAW;IACpD,OAAO,YAAY,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,+BAA+B,CAAC,GAAW;IACvD,OAAO,YAAY,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;AAClD,CAAC;AASD,MAAM,aAAa,GAAgB;IAC/B,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,0BAA0B,EAAE;IACtE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,0BAA0B,EAAE;IACxE,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,4BAA4B,EAAE;IAC3E,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,+BAA+B,EAAE;CACrF,CAAC;AAEF,MAAM,aAAa,GAAgB;IAC/B,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,qBAAqB,CAAC,EAAE;IAChG,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,mBAAmB,CAAC,EAAE;CACrG,CAAC;AAEF,SAAS,OAAO,CAAC,EAAY,EAAE,KAAqC,EAAE,QAAgB,EAAE,MAAmB;IACvG,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,UAAU,QAAQ,aAAa,OAAO,SAAS,KAAK,EAAE,CAAC,CAAC,GAAG,EAA4C,CAAC;IAEhI,MAAM,OAAO,GAAqB,EAAE,CAAC;IACrC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACrB,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;YACxB,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC/B,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;gBAC7B,SAAS;YACb,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YACrC,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;gBACnB,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;YACvF,CAAC;QACL,CAAC;IACL,CAAC;IACD,OAAO,OAAO,CAAC;AACnB,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,YAAY,CAAC,EAAY;IACrC,MAAM,OAAO,GAAG,CAAC,GAAG,OAAO,CAAC,EAAE,EAAE,iBAAiB,EAAE,YAAY,EAAE,aAAa,CAAC,EAAE,GAAG,OAAO,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC;IAClI,OAAO;QACH,OAAO;QACP,UAAU,EAAE,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,iBAAiB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI;QAClG,UAAU,EAAE,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI;KAC9F,CAAC;AACN,CAAC;AAED,6GAA6G;AAC7G,MAAM,UAAU,aAAa,CAAC,EAAY;IACtC,MAAM,IAAI,GAAG,YAAY,CAAC,EAAE,CAAC,CAAC;IAC9B,MAAM,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;QAC9B,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAC3B,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,KAAK,iBAAiB,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC;YACrE,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,KAAK,cAAc,QAAQ,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;QACnG,CAAC;IACL,CAAC,CAAC,CAAC;IACH,KAAK,EAAE,CAAC;IACR,OAAO,IAAI,CAAC;AAChB,CAAC;AASD;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,EAAY;IACvC,MAAM,OAAO,GAAsB,EAAE,CAAC;IACtC,MAAM,KAAK,GAAG,CAAC,KAAqC,EAAE,QAAgB,EAAE,MAAmB,EAAE,EAAE;QAC3F,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,UAAU,QAAQ,aAAa,OAAO,SAAS,KAAK,EAAE,CAAC,CAAC,GAAG,EAA4C,CAAC;QAChI,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACrB,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;gBACxB,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAC5B,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;oBAC1B,SAAS;gBACb,CAAC;gBACD,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC9C,IAAI,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC;wBAC3B,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;oBACrF,CAAC;gBACL,CAAC;YACL,CAAC;QACL,CAAC;IACL,CAAC,CAAC;IACF,KAAK,CAAC,iBAAiB,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC;IACtD,KAAK,CAAC,UAAU,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;IACvC,OAAO,OAAO,CAAC;AACnB,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,WAAW,CAAC,GAAW,EAAE,MAAe;IAC7C,IAAI,CAAC,MAAM,EAAE,CAAC;QACV,OAAO,CAAC,GAAG,CAAC,CAAC;IACjB,CAAC;IACD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACD,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,wDAAwD;IAC1E,CAAC;IACD,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,IAAI,GAAG,CAAC,IAAa,EAAQ,EAAE;QACjC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC3B,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;aAAM,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7B,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;gBACvB,IAAI,CAAC,KAAK,CAAC,CAAC;YAChB,CAAC;QACL,CAAC;aAAM,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC1C,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;gBACtC,IAAI,CAAC,KAAK,CAAC,CAAC;YAChB,CAAC;QACL,CAAC;IACL,CAAC,CAAC;IACF,IAAI,CAAC,MAAM,CAAC,CAAC;IACb,OAAO,GAAG,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Segmentation boundary evaluator. Pure decision function - no I/O, no DB
|
|
2
|
+
// access. Callers assemble SegmentBoundaryInput from denormalized
|
|
3
|
+
// trailing_branch/trailing_files columns (see MemoryStore.updateTrailingState)
|
|
4
|
+
// plus the resuming turn's gitBranch/files_touched/resumeMarkerBefore, keeping
|
|
5
|
+
// the decision to one row read per closed turn.
|
|
6
|
+
//
|
|
7
|
+
// - gap >= 7 days -> cut unconditionally; overlap is diagnostically unavailable.
|
|
8
|
+
// - below 7 days, files_touched overlap >= 0.5 vetoes a cut.
|
|
9
|
+
// - otherwise, gap >= 4h AND discontinuity evidence -> cut. Evidence: branch
|
|
10
|
+
// differs, OR files_touched overlap below threshold, OR a Codex resume
|
|
11
|
+
// marker (<environment_context>) immediately preceded the
|
|
12
|
+
// resuming turn. Known-equal branches suppress file overlap when it is the
|
|
13
|
+
// only evidence.
|
|
14
|
+
// - gap >= 4h with no evidence -> no cut (resume-tomorrow guard).
|
|
15
|
+
// - gap < 4h -> never cut, regardless of evidence. The marker does not
|
|
16
|
+
// override this - a 36-second restart to change --model is a real
|
|
17
|
+
// process reattachment inside one episode, not an episode boundary.
|
|
18
|
+
import { SEGMENT_FILE_CONTINUITY_THRESHOLD, SEGMENT_FILE_OVERLAP_THRESHOLD, SEGMENT_MIN_GAP_HOURS, SEGMENT_UNCONDITIONAL_GAP_HOURS, } from '../config/constants.js';
|
|
19
|
+
/**
|
|
20
|
+
* Jaccard overlap (intersection / union) of the two file sets. Returns null
|
|
21
|
+
* when either side is empty - that is "unavailable", never "differs". With
|
|
22
|
+
* nothing on one side there is nothing to compare; treating the arithmetic
|
|
23
|
+
* zero as maximum divergence would turn missing file data into cut evidence,
|
|
24
|
+
* especially for Codex where most turns carry no paths.
|
|
25
|
+
*/
|
|
26
|
+
export function fileOverlapRatio(trailingFiles, resumingFiles) {
|
|
27
|
+
if (trailingFiles.length === 0 || resumingFiles.length === 0) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const trailing = new Set(trailingFiles);
|
|
31
|
+
const resuming = new Set(resumingFiles);
|
|
32
|
+
let intersection = 0;
|
|
33
|
+
for (const f of trailing) {
|
|
34
|
+
if (resuming.has(f)) {
|
|
35
|
+
intersection++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const union = new Set([...trailing, ...resuming]).size;
|
|
39
|
+
return union === 0 ? null : intersection / union;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* True if branch is known on both sides and they differ. An unknown branch on
|
|
43
|
+
* either side is never treated as "differs" - Codex has no per-turn branch
|
|
44
|
+
* capture (see corpus-discriminators), and the Codex fallback rule ("no cut
|
|
45
|
+
* below 7 days when no evidence exists") depends on that absence not being
|
|
46
|
+
* misread as a positive discontinuity signal.
|
|
47
|
+
*/
|
|
48
|
+
function branchDiffers(trailingBranch, resumingBranch) {
|
|
49
|
+
return trailingBranch !== null && resumingBranch !== null && trailingBranch !== resumingBranch;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Diagnostic companion to evaluateSegmentBoundary(). Migration previews need
|
|
53
|
+
* to show why each proposed cut fired, not only the aggregate count. The
|
|
54
|
+
* evaluator delegates to this function so preview evidence cannot drift from
|
|
55
|
+
* the live decision rule.
|
|
56
|
+
*/
|
|
57
|
+
export function assessSegmentBoundary(input) {
|
|
58
|
+
if (input.gapHours >= SEGMENT_UNCONDITIONAL_GAP_HOURS) {
|
|
59
|
+
return { cut: true, evidence: ['seven-day-gap'], fileOverlap: null };
|
|
60
|
+
}
|
|
61
|
+
const overlap = fileOverlapRatio(input.trailingFiles, input.resumingFiles);
|
|
62
|
+
if (input.gapHours < SEGMENT_MIN_GAP_HOURS) {
|
|
63
|
+
return { cut: false, evidence: [], fileOverlap: overlap };
|
|
64
|
+
}
|
|
65
|
+
if (overlap !== null && overlap >= SEGMENT_FILE_CONTINUITY_THRESHOLD) {
|
|
66
|
+
return { cut: false, evidence: [], fileOverlap: overlap };
|
|
67
|
+
}
|
|
68
|
+
const evidence = [];
|
|
69
|
+
if (branchDiffers(input.trailingBranch, input.resumingBranch)) {
|
|
70
|
+
evidence.push('branch-change');
|
|
71
|
+
}
|
|
72
|
+
if (overlap !== null && overlap < SEGMENT_FILE_OVERLAP_THRESHOLD) {
|
|
73
|
+
evidence.push('file-overlap');
|
|
74
|
+
}
|
|
75
|
+
if (input.resumeMarkerBefore) {
|
|
76
|
+
evidence.push('resume-marker');
|
|
77
|
+
}
|
|
78
|
+
const branchesMatch = input.trailingBranch !== null && input.resumingBranch !== null && input.trailingBranch === input.resumingBranch;
|
|
79
|
+
if (branchesMatch && evidence.length === 1 && evidence[0] === 'file-overlap') {
|
|
80
|
+
return { cut: false, evidence: [], fileOverlap: overlap };
|
|
81
|
+
}
|
|
82
|
+
return { cut: evidence.length > 0, evidence, fileOverlap: overlap };
|
|
83
|
+
}
|
|
84
|
+
export function evaluateSegmentBoundary(input) {
|
|
85
|
+
return assessSegmentBoundary(input).cut;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=segmentation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segmentation.js","sourceRoot":"","sources":["../../src/storage/segmentation.ts"],"names":[],"mappings":"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"}
|