@inerrata-corporation/errata 2.0.23-dev.4734 → 2.0.23-dev.4796
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/errata.mjs
CHANGED
|
@@ -20645,7 +20645,8 @@ var init_node_select = __esm({
|
|
|
20645
20645
|
"nodes_live_fold_candidate",
|
|
20646
20646
|
"nodes_live_resolved",
|
|
20647
20647
|
"nodes_live_fix_candidate",
|
|
20648
|
-
"nodes_live_anchor_hint"
|
|
20648
|
+
"nodes_live_anchor_hint",
|
|
20649
|
+
"nodes_live_created_v2"
|
|
20649
20650
|
];
|
|
20650
20651
|
EMBEDDING_DIM_SQL = "(CASE WHEN length(embedding_blob) >= 4 THEN length(embedding_blob) / 4 ELSE COALESCE(json_array_length(embedding_json), 0) END)";
|
|
20651
20652
|
KEY = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
@@ -20761,10 +20762,15 @@ function sameSqlValue(a, b) {
|
|
|
20761
20762
|
if (a instanceof Uint8Array && b instanceof Uint8Array) return Buffer.compare(a, b) === 0;
|
|
20762
20763
|
return false;
|
|
20763
20764
|
}
|
|
20765
|
+
function attrsJsonWithoutPositions(attrs) {
|
|
20766
|
+
const rest2 = { ...attrs };
|
|
20767
|
+
for (const k of POSITION_ATTRS) delete rest2[k];
|
|
20768
|
+
return JSON.stringify(rest2);
|
|
20769
|
+
}
|
|
20764
20770
|
function openGraphStore(opts) {
|
|
20765
20771
|
return new SqliteGraphStore(opts);
|
|
20766
20772
|
}
|
|
20767
|
-
var PAGE_RANK_SCALE_KEY, TEXT_SEARCH_COLUMNS, LOCAL_RULE_OVERRIDES, SCHEMA_VERSION, SESSION_REPAIR_KEY, EXPOSURE_SIDE_KEY, SESSION_EVIDENCE_PREDICATE, SCHEMA_SQL, TEMPORAL_INDEX_SQL, FINGERPRINT_INDEXED_LABELS, NODE_LABELS_CHUNK, NODE_UPSERT_COLUMNS, EDGE_UPSERT_COLUMNS, PRIOR_TAG_MARKER, SqliteGraphStore;
|
|
20773
|
+
var PAGE_RANK_SCALE_KEY, TEXT_SEARCH_COLUMNS, LOCAL_RULE_OVERRIDES, SCHEMA_VERSION, SESSION_REPAIR_KEY, EXPOSURE_SIDE_KEY, SESSION_EVIDENCE_PREDICATE, SCHEMA_SQL, TEMPORAL_INDEX_SQL, FINGERPRINT_INDEXED_LABELS, NODE_LABELS_CHUNK, NODE_UPSERT_COLUMNS, EDGE_UPSERT_COLUMNS, POSITION_ATTRS, PRIOR_TAG_MARKER, SqliteGraphStore;
|
|
20768
20774
|
var init_store = __esm({
|
|
20769
20775
|
"../../packages/local-graph/src/store.ts"() {
|
|
20770
20776
|
"use strict";
|
|
@@ -20918,7 +20924,10 @@ CREATE UNIQUE INDEX IF NOT EXISTS edges_triple ON edges(from_id, type, to_id);
|
|
|
20918
20924
|
`;
|
|
20919
20925
|
TEMPORAL_INDEX_SQL = `
|
|
20920
20926
|
CREATE INDEX IF NOT EXISTS nodes_live ON nodes(label, valid_to);
|
|
20921
|
-
|
|
20927
|
+
-- Dropped: a prefix of nodes_live_page_rank (valid_to, page_rank), which serves
|
|
20928
|
+
-- the live count and the "any live node?" check as a covering index too. It was
|
|
20929
|
+
-- re-keyed on every close and revive: ~11 KB of a symbol flip's ~80 KB.
|
|
20930
|
+
DROP INDEX IF EXISTS nodes_live_count;
|
|
20922
20931
|
-- Problems by recency (the only label read newest-first). Over every live row,
|
|
20923
20932
|
-- each write that stamps last_updated_at moved a code symbol's entry from a
|
|
20924
20933
|
-- scattered leaf: ~19 KB per position-only symbol rewrite on a 1.1 GB store.
|
|
@@ -20930,7 +20939,13 @@ CREATE INDEX IF NOT EXISTS nodes_live_problem_recent ON nodes(label, last_update
|
|
|
20930
20939
|
-- walks nodes_live and reads every live row of the label to test created_at:
|
|
20931
20940
|
-- 56 MB per guard on a 1.1 GB store, to learn there was nothing to do. valid_to
|
|
20932
20941
|
-- is a key column so the planner prefers this over nodes_live (one more term).
|
|
20933
|
-
|
|
20942
|
+
-- last_updated_at rides along for errata.recent on code labels, which names this
|
|
20943
|
+
-- index: the filter runs on index entries, not on every row of the label (79 ms
|
|
20944
|
+
-- \u2192 3 ms for Function on a 1.1 GB store). It costs no extra writes: a close or
|
|
20945
|
+
-- revive already re-keys this index, and mergeNodeIfChanged leaves a symbol's
|
|
20946
|
+
-- last_updated_at alone when only its position moved.
|
|
20947
|
+
DROP INDEX IF EXISTS nodes_live_created;
|
|
20948
|
+
CREATE INDEX IF NOT EXISTS nodes_live_created_v2 ON nodes(label, valid_to, created_at, last_updated_at) WHERE valid_to IS NULL;
|
|
20934
20949
|
-- Community members (the skills pull reads them twice per session boundary):
|
|
20935
20950
|
-- ~15% of semantic nodes carry a localCommunity, and without this every live
|
|
20936
20951
|
-- semantic row was read to find them (168 MB \u2192 18 MB per read).
|
|
@@ -21053,6 +21068,7 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
|
|
|
21053
21068
|
"attrs_json"
|
|
21054
21069
|
];
|
|
21055
21070
|
EDGE_UPSERT_COLUMNS = ["confidence", "last_seen_at", "nav_successes", "nav_failures", "valid_to", "attrs_json"];
|
|
21071
|
+
POSITION_ATTRS = ["startByte", "endByte", "startRow"];
|
|
21056
21072
|
PRIOR_TAG_MARKER = '"priorTag":true';
|
|
21057
21073
|
SqliteGraphStore = class {
|
|
21058
21074
|
db;
|
|
@@ -21153,7 +21169,7 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
|
|
|
21153
21169
|
lastUpdatedOf: this.db.prepare("SELECT last_updated_at AS t, attrs_json FROM nodes WHERE id = ?"),
|
|
21154
21170
|
labelOf: this.db.prepare("SELECT label FROM nodes WHERE id = ?"),
|
|
21155
21171
|
nodeContent: this.db.prepare(
|
|
21156
|
-
"SELECT label, description, extraction_confidence, extraction_source, stability, valid_to, attrs_json FROM nodes WHERE id = ?"
|
|
21172
|
+
"SELECT label, description, extraction_confidence, extraction_source, stability, valid_to, last_updated_at, attrs_json FROM nodes WHERE id = ?"
|
|
21157
21173
|
),
|
|
21158
21174
|
edgeContent: this.db.prepare(
|
|
21159
21175
|
"SELECT confidence, valid_to, attrs_json FROM edges WHERE from_id = ? AND type = ? AND to_id = ?"
|
|
@@ -21566,11 +21582,23 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
|
|
|
21566
21582
|
* rank, community, landmark, surprise, hits, tier and the vector belong to
|
|
21567
21583
|
* the passes that compute them, and a skipped write leaves them as they are
|
|
21568
21584
|
* where a merge would have reset them to the producer's defaults. Returns
|
|
21569
|
-
* whether it wrote.
|
|
21585
|
+
* whether it wrote.
|
|
21586
|
+
*
|
|
21587
|
+
* A live row whose only change is its position keeps its lastUpdatedAt, so
|
|
21588
|
+
* that stamp means "this symbol's content changed": what errata.recent lists
|
|
21589
|
+
* and the fix-candidate ask reads, and what lets nodes_live_created_v2 carry
|
|
21590
|
+
* it without being re-keyed by every edit above a symbol. A closed row is
|
|
21591
|
+
* always stamped: reviveReobserved reopens what this pass stamped. */
|
|
21570
21592
|
mergeNodeIfChanged(node2) {
|
|
21571
21593
|
const row = this.stmts.nodeContent.get(node2.id);
|
|
21572
|
-
if (row
|
|
21573
|
-
|
|
21594
|
+
if (!row || row.valid_to !== null || row.label !== node2.label || row.description !== node2.description || row.extraction_confidence !== node2.extractionConfidence || row.stability !== node2.stability || EXPOSURE_ATTRS.some((k) => node2.attrs?.[k] !== void 0)) {
|
|
21595
|
+
this.mergeNode(node2);
|
|
21596
|
+
return true;
|
|
21597
|
+
}
|
|
21598
|
+
const attrs = node2.attrs ?? {};
|
|
21599
|
+
if (row.attrs_json === JSON.stringify(attrs)) return false;
|
|
21600
|
+
const moved = attrsJsonWithoutPositions(JSON.parse(row.attrs_json)) === attrsJsonWithoutPositions(attrs);
|
|
21601
|
+
this.mergeNode(moved ? { ...node2, lastUpdatedAt: row.last_updated_at } : node2);
|
|
21574
21602
|
return true;
|
|
21575
21603
|
}
|
|
21576
21604
|
updateNode(id, patch) {
|
|
@@ -21650,8 +21678,8 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
|
|
|
21650
21678
|
stmt2 = this.db.prepare(
|
|
21651
21679
|
// Ordered, not left to the planner: readers take "first row wins" (the
|
|
21652
21680
|
// symbol lexicon's name winner and doc pick), and the order otherwise
|
|
21653
|
-
// followed whichever partial index the planner chose.
|
|
21654
|
-
// serves
|
|
21681
|
+
// followed whichever partial index the planner chose. nodes_live_created_v2
|
|
21682
|
+
// serves it, sorting rowids only within an equal created_at.
|
|
21655
21683
|
`SELECT id, description, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL ORDER BY created_at, rowid`
|
|
21656
21684
|
);
|
|
21657
21685
|
this.fieldStmts.set(cacheKey2, stmt2);
|
|
@@ -23199,10 +23227,12 @@ function crystallize(store, opts = { ts: 0 }) {
|
|
|
23199
23227
|
const s = support(c) + Math.max(0, sourcesCount(c) - 1);
|
|
23200
23228
|
const f = contradictions;
|
|
23201
23229
|
const confidence = (1 + s) / (2 + s + f);
|
|
23202
|
-
|
|
23203
|
-
|
|
23204
|
-
|
|
23205
|
-
|
|
23230
|
+
if (c.attrs["confidence"] !== confidence) {
|
|
23231
|
+
store.updateNode(c.id, {
|
|
23232
|
+
attrs: { ...c.attrs, confidence },
|
|
23233
|
+
lastUpdatedAt: opts.ts
|
|
23234
|
+
});
|
|
23235
|
+
}
|
|
23206
23236
|
const sup = support(c);
|
|
23207
23237
|
let tier = c.memoryTier;
|
|
23208
23238
|
if (sup >= PERSISTENT_SUPPORT && confidence >= PERSISTENT_CONFIDENCE) tier = "persistent";
|
|
@@ -23270,7 +23300,10 @@ function mergeTruthKind(a, b) {
|
|
|
23270
23300
|
function markBoth(store, a, b, patch, ts) {
|
|
23271
23301
|
for (const n of [a, b]) {
|
|
23272
23302
|
const fresh = store.getNode(n.id);
|
|
23273
|
-
|
|
23303
|
+
const next = { ...patch };
|
|
23304
|
+
if (fresh.attrs["revisit"] === true && fresh.attrs["revisitReason"] === next["revisitReason"]) delete next["revisitSinceTs"];
|
|
23305
|
+
if (Object.entries(next).every(([k, v]) => fresh.attrs[k] === v)) continue;
|
|
23306
|
+
store.updateNode(n.id, { attrs: { ...fresh.attrs, ...next }, lastUpdatedAt: ts });
|
|
23274
23307
|
}
|
|
23275
23308
|
}
|
|
23276
23309
|
var LONG_TERM_SUPPORT, PERSISTENT_SUPPORT, PERSISTENT_CONFIDENCE, SCOPE_AXES;
|
|
@@ -37854,26 +37887,26 @@ function buildFileLexicon(store) {
|
|
|
37854
37887
|
for (const n of store.findNodeFieldsByLabel("File", ["relPath"])) {
|
|
37855
37888
|
const relPath = typeof n.attrs["relPath"] === "string" && n.attrs["relPath"].length > 0 ? n.attrs["relPath"] : n.description;
|
|
37856
37889
|
if (!relPath) continue;
|
|
37857
|
-
const
|
|
37858
|
-
if (!
|
|
37859
|
-
entries.push({ relPath, basename:
|
|
37860
|
-
basenameCount.set(
|
|
37861
|
-
const stem =
|
|
37890
|
+
const basename11 = relPath.split("/").pop();
|
|
37891
|
+
if (!basename11.includes(".")) continue;
|
|
37892
|
+
entries.push({ relPath, basename: basename11 });
|
|
37893
|
+
basenameCount.set(basename11, (basenameCount.get(basename11) ?? 0) + 1);
|
|
37894
|
+
const stem = basename11.slice(0, basename11.indexOf("."));
|
|
37862
37895
|
if (isBareMatchableStem(stem)) {
|
|
37863
37896
|
let s = stemRelPaths.get(stem);
|
|
37864
37897
|
if (!s) stemRelPaths.set(stem, s = /* @__PURE__ */ new Set());
|
|
37865
37898
|
s.add(relPath);
|
|
37866
37899
|
}
|
|
37867
37900
|
}
|
|
37868
|
-
for (const { relPath, basename:
|
|
37901
|
+
for (const { relPath, basename: basename11 } of entries) {
|
|
37869
37902
|
const entry = { relPath };
|
|
37870
|
-
if (relPath !==
|
|
37871
|
-
const conventional = CONVENTION_FILENAMES.has(
|
|
37872
|
-
if (!conventional && !byKey.has(
|
|
37873
|
-
const list = byBasename.get(
|
|
37903
|
+
if (relPath !== basename11) byKey.set(relPath, entry);
|
|
37904
|
+
const conventional = CONVENTION_FILENAMES.has(basename11) || (basenameCount.get(basename11) ?? 0) >= CONVENTION_BASENAME_MIN_FILES;
|
|
37905
|
+
if (!conventional && !byKey.has(basename11)) byKey.set(basename11, entry);
|
|
37906
|
+
const list = byBasename.get(basename11);
|
|
37874
37907
|
if (list) list.push(entry);
|
|
37875
|
-
else byBasename.set(
|
|
37876
|
-
const stem =
|
|
37908
|
+
else byBasename.set(basename11, [entry]);
|
|
37909
|
+
const stem = basename11.slice(0, basename11.indexOf("."));
|
|
37877
37910
|
if (isBareMatchableStem(stem) && !CONVENTION_STEMS.has(stem) && !byStem.has(stem)) {
|
|
37878
37911
|
const dirStems = new Set(
|
|
37879
37912
|
[...stemRelPaths.get(stem) ?? []].map((p) => {
|
|
@@ -37890,8 +37923,8 @@ function resolveFileToken(token, lexicon) {
|
|
|
37890
37923
|
const exact = lexicon.byKey.get(token);
|
|
37891
37924
|
if (exact) return exact;
|
|
37892
37925
|
if (!token.includes("/")) return null;
|
|
37893
|
-
const
|
|
37894
|
-
for (const entry of lexicon.byBasename.get(
|
|
37926
|
+
const basename11 = token.split("/").pop();
|
|
37927
|
+
for (const entry of lexicon.byBasename.get(basename11) ?? []) {
|
|
37895
37928
|
if (entry.relPath === token || entry.relPath.endsWith(`/${token}`)) return entry;
|
|
37896
37929
|
}
|
|
37897
37930
|
return null;
|
|
@@ -37916,9 +37949,9 @@ function fileRolePhrase(relPath, level = 1) {
|
|
|
37916
37949
|
return "a source file";
|
|
37917
37950
|
}
|
|
37918
37951
|
function isSourceFileToken(token) {
|
|
37919
|
-
const
|
|
37920
|
-
if (CONVENTION_FILENAMES.has(
|
|
37921
|
-
const ext =
|
|
37952
|
+
const basename11 = token.split(/[/\\]/).pop() ?? token;
|
|
37953
|
+
if (CONVENTION_FILENAMES.has(basename11)) return false;
|
|
37954
|
+
const ext = basename11.split(".").pop();
|
|
37922
37955
|
return ext !== void 0 && SOURCE_EXTENSIONS.has(ext.toLowerCase());
|
|
37923
37956
|
}
|
|
37924
37957
|
function generalizeFileRefs(text, fileLexicon, level = 1) {
|
|
@@ -49322,7 +49355,14 @@ var init_mcp = __esm({
|
|
|
49322
49355
|
total += count;
|
|
49323
49356
|
groups[label] = {
|
|
49324
49357
|
count,
|
|
49325
|
-
items: store.selectNodes({
|
|
49358
|
+
items: store.selectNodes({
|
|
49359
|
+
...q,
|
|
49360
|
+
orderBy: { column: col, desc: true },
|
|
49361
|
+
limit: sliceEnd(limit),
|
|
49362
|
+
// Unnamed, the planner reads every row of a code label through
|
|
49363
|
+
// nodes_live to test last_updated_at; this index carries it.
|
|
49364
|
+
...col === "last_updated_at" ? { indexedBy: "nodes_live_created_v2" } : {}
|
|
49365
|
+
}).map((n) => ({ id: n.id, description: n.description.slice(0, 160), at: col === "last_updated_at" ? n.lastUpdatedAt : n.createdAt }))
|
|
49326
49366
|
};
|
|
49327
49367
|
}
|
|
49328
49368
|
}
|
|
@@ -57689,6 +57729,7 @@ var init_message_accumulator = __esm({
|
|
|
57689
57729
|
var webui_exports = {};
|
|
57690
57730
|
__export(webui_exports, {
|
|
57691
57731
|
ContextBaselines: () => ContextBaselines,
|
|
57732
|
+
FOCUS_NOTICE_FIELD: () => FOCUS_NOTICE_FIELD,
|
|
57692
57733
|
SUBAGENT_REFRESH_MIN_MS: () => SUBAGENT_REFRESH_MIN_MS,
|
|
57693
57734
|
asFailedPostToolUse: () => asFailedPostToolUse,
|
|
57694
57735
|
buildWebUi: () => buildWebUi,
|
|
@@ -58398,7 +58439,9 @@ ${delta}${cue}`;
|
|
|
58398
58439
|
block = own ?? readContextFileWithHash(deps.paths.root);
|
|
58399
58440
|
storedCard = own ? own.card : block ? readContextCard(deps.paths.root, block.hash) : null;
|
|
58400
58441
|
}
|
|
58401
|
-
const
|
|
58442
|
+
const focusNotice = typeof body2[FOCUS_NOTICE_FIELD] === "string" ? body2[FOCUS_NOTICE_FIELD] : null;
|
|
58443
|
+
if (focusNotice && contextKey2) lastInjectedHash.reset(contextKey2, "focus");
|
|
58444
|
+
const decided = decideContextInject({
|
|
58402
58445
|
event: hookEvent,
|
|
58403
58446
|
source: String(body2["source"] ?? ""),
|
|
58404
58447
|
sessionId: contextKey2,
|
|
@@ -58408,6 +58451,9 @@ ${delta}${cue}`;
|
|
|
58408
58451
|
// A subagent's injections land in its own transcript, not the one posted.
|
|
58409
58452
|
...subagent === null && typeof body2["transcript_path"] === "string" && body2["transcript_path"] ? { transcriptPath: body2["transcript_path"] } : {}
|
|
58410
58453
|
});
|
|
58454
|
+
const decision = focusNotice ? { hookEventName: decided?.hookEventName ?? hookEvent, additionalContext: decided ? `${focusNotice}
|
|
58455
|
+
|
|
58456
|
+
${decided.additionalContext}` : focusNotice } : decided;
|
|
58411
58457
|
if (decision && subagent !== null) lastSubagentInjectAt.set(contextKey2, Date.now());
|
|
58412
58458
|
let systemMessage = null;
|
|
58413
58459
|
const delivered = decision !== null && decision.additionalContext !== PROMPT_RECUE;
|
|
@@ -58606,7 +58652,7 @@ function proposalToIngest(p, profile, daemonVersion) {
|
|
|
58606
58652
|
payloadDigest: p.id
|
|
58607
58653
|
};
|
|
58608
58654
|
}
|
|
58609
|
-
var messageAccumulator, GENERIC_ERROR_WORDS, wsPackagesCache, WS_PACKAGES_TTL_MS, loggedToolSessions, SUBAGENT_REFRESH_MIN_MS, WINDOW_RESET_SOURCES, FULL_BLOCK_PREFIX, META_LABELS;
|
|
58655
|
+
var messageAccumulator, GENERIC_ERROR_WORDS, wsPackagesCache, WS_PACKAGES_TTL_MS, loggedToolSessions, SUBAGENT_REFRESH_MIN_MS, WINDOW_RESET_SOURCES, FULL_BLOCK_PREFIX, FOCUS_NOTICE_FIELD, META_LABELS;
|
|
58610
58656
|
var init_webui = __esm({
|
|
58611
58657
|
"src/webui.ts"() {
|
|
58612
58658
|
"use strict";
|
|
@@ -58673,6 +58719,7 @@ var init_webui = __esm({
|
|
|
58673
58719
|
SUBAGENT_REFRESH_MIN_MS = 6e4;
|
|
58674
58720
|
WINDOW_RESET_SOURCES = /* @__PURE__ */ new Set(["compact", "clear", "resume"]);
|
|
58675
58721
|
FULL_BLOCK_PREFIX = "The errata daemon has updated your live workspace context. The block below REPLACES any earlier errata context block in this conversation \u2014 treat it as the current source of truth:\n\n";
|
|
58722
|
+
FOCUS_NOTICE_FIELD = "errata_focus_notice";
|
|
58676
58723
|
META_LABELS = /* @__PURE__ */ new Set([
|
|
58677
58724
|
"Pattern",
|
|
58678
58725
|
"Technique",
|
|
@@ -78818,10 +78865,10 @@ var ReaddirpStream = class extends Readable2 {
|
|
|
78818
78865
|
}
|
|
78819
78866
|
async _formatEntry(dirent, path3) {
|
|
78820
78867
|
let entry;
|
|
78821
|
-
const
|
|
78868
|
+
const basename11 = this._isDirent ? dirent.name : dirent;
|
|
78822
78869
|
try {
|
|
78823
|
-
const fullPath = presolve(pjoin(path3,
|
|
78824
|
-
entry = { path: prelative(this._root, fullPath), fullPath, basename:
|
|
78870
|
+
const fullPath = presolve(pjoin(path3, basename11));
|
|
78871
|
+
entry = { path: prelative(this._root, fullPath), fullPath, basename: basename11 };
|
|
78825
78872
|
entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
|
|
78826
78873
|
} catch (err2) {
|
|
78827
78874
|
this._onError(err2);
|
|
@@ -79360,9 +79407,9 @@ var NodeFsHandler = class {
|
|
|
79360
79407
|
_watchWithNodeFs(path3, listener) {
|
|
79361
79408
|
const opts = this.fsw.options;
|
|
79362
79409
|
const directory = sysPath.dirname(path3);
|
|
79363
|
-
const
|
|
79410
|
+
const basename11 = sysPath.basename(path3);
|
|
79364
79411
|
const parent = this.fsw._getWatchedDir(directory);
|
|
79365
|
-
parent.add(
|
|
79412
|
+
parent.add(basename11);
|
|
79366
79413
|
const absolutePath = sysPath.resolve(path3);
|
|
79367
79414
|
const options = {
|
|
79368
79415
|
persistent: opts.persistent
|
|
@@ -79372,7 +79419,7 @@ var NodeFsHandler = class {
|
|
|
79372
79419
|
let closer;
|
|
79373
79420
|
if (opts.usePolling) {
|
|
79374
79421
|
const enableBin = opts.interval !== opts.binaryInterval;
|
|
79375
|
-
options.interval = enableBin && isBinaryPath(
|
|
79422
|
+
options.interval = enableBin && isBinaryPath(basename11) ? opts.binaryInterval : opts.interval;
|
|
79376
79423
|
closer = setFsWatchFileListener(path3, absolutePath, options, {
|
|
79377
79424
|
listener,
|
|
79378
79425
|
rawEmitter: this.fsw._emitRaw
|
|
@@ -79395,10 +79442,10 @@ var NodeFsHandler = class {
|
|
|
79395
79442
|
return;
|
|
79396
79443
|
}
|
|
79397
79444
|
const dirname21 = sysPath.dirname(file2);
|
|
79398
|
-
const
|
|
79445
|
+
const basename11 = sysPath.basename(file2);
|
|
79399
79446
|
const parent = this.fsw._getWatchedDir(dirname21);
|
|
79400
79447
|
let prevStats = stats;
|
|
79401
|
-
if (parent.has(
|
|
79448
|
+
if (parent.has(basename11))
|
|
79402
79449
|
return;
|
|
79403
79450
|
const listener = async (path3, newStats) => {
|
|
79404
79451
|
if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file2, 5))
|
|
@@ -79423,9 +79470,9 @@ var NodeFsHandler = class {
|
|
|
79423
79470
|
prevStats = newStats2;
|
|
79424
79471
|
}
|
|
79425
79472
|
} catch (error48) {
|
|
79426
|
-
this.fsw._remove(dirname21,
|
|
79473
|
+
this.fsw._remove(dirname21, basename11);
|
|
79427
79474
|
}
|
|
79428
|
-
} else if (parent.has(
|
|
79475
|
+
} else if (parent.has(basename11)) {
|
|
79429
79476
|
const at = newStats.atimeMs;
|
|
79430
79477
|
const mt = newStats.mtimeMs;
|
|
79431
79478
|
if (!at || at <= mt || mt !== prevStats.mtimeMs) {
|
|
@@ -85606,7 +85653,7 @@ var OntologyValidationError = class extends Error {
|
|
|
85606
85653
|
}
|
|
85607
85654
|
};
|
|
85608
85655
|
function phrase(text, article) {
|
|
85609
|
-
const capitalized =
|
|
85656
|
+
const capitalized = firstAlphaCharUpper(text);
|
|
85610
85657
|
return { text, article, textCapitalized: capitalized };
|
|
85611
85658
|
}
|
|
85612
85659
|
var DEFAULT_ONTOLOGY2 = {
|
|
@@ -85729,7 +85776,7 @@ function expectedArticleKind(text) {
|
|
|
85729
85776
|
return "bare";
|
|
85730
85777
|
}
|
|
85731
85778
|
function firstAlphaCharUpper(s) {
|
|
85732
|
-
return s.
|
|
85779
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
85733
85780
|
}
|
|
85734
85781
|
function phraseTriggersLayer1(text) {
|
|
85735
85782
|
for (const pat of ALL_PATTERNS) {
|
|
@@ -87480,7 +87527,7 @@ function createWatchBreaker(deps) {
|
|
|
87480
87527
|
}
|
|
87481
87528
|
|
|
87482
87529
|
// src/engine.ts
|
|
87483
|
-
var DAEMON_VERSION = true ? "2.0.23-dev.
|
|
87530
|
+
var DAEMON_VERSION = true ? "2.0.23-dev.4796" : "2.0.0-alpha.0";
|
|
87484
87531
|
var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
|
|
87485
87532
|
var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
|
|
87486
87533
|
var GIT_OP_MUTE_MS = 4e3;
|
|
@@ -87523,15 +87570,43 @@ function loadTurnOffsets(path3) {
|
|
|
87523
87570
|
return /* @__PURE__ */ new Map();
|
|
87524
87571
|
}
|
|
87525
87572
|
}
|
|
87526
|
-
function
|
|
87573
|
+
function loadTurnMoves(path3) {
|
|
87574
|
+
try {
|
|
87575
|
+
const raw2 = JSON.parse(readFileSync33(path3, "utf8"));
|
|
87576
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
87577
|
+
for (const [k, v] of Object.entries(raw2)) {
|
|
87578
|
+
const to = typeof v === "object" && v !== null ? v.movedTo : void 0;
|
|
87579
|
+
if (typeof to === "string" && to) out2.set(k, to);
|
|
87580
|
+
}
|
|
87581
|
+
return out2;
|
|
87582
|
+
} catch {
|
|
87583
|
+
return /* @__PURE__ */ new Map();
|
|
87584
|
+
}
|
|
87585
|
+
}
|
|
87586
|
+
function saveTurnCursors(path3, cursors, offsets, moves = /* @__PURE__ */ new Map()) {
|
|
87527
87587
|
try {
|
|
87528
87588
|
const merged = {};
|
|
87529
87589
|
for (const [k, uuid3] of cursors) merged[k] = { uuid: uuid3, offset: offsets.get(k) ?? 0 };
|
|
87530
87590
|
for (const [k, offset] of offsets) if (!merged[k]) merged[k] = { uuid: "", offset };
|
|
87591
|
+
for (const [k, movedTo] of moves) merged[k] = { ...merged[k] ?? { uuid: "", offset: 0 }, movedTo };
|
|
87531
87592
|
writeFileSync24(path3, JSON.stringify(merged), "utf8");
|
|
87532
87593
|
} catch {
|
|
87533
87594
|
}
|
|
87534
87595
|
}
|
|
87596
|
+
var turnCursorFile = (configDir) => join37(configDir, "turn-cursors.json");
|
|
87597
|
+
function turnCursorOnDisk(configDir, sessionId) {
|
|
87598
|
+
const path3 = turnCursorFile(configDir);
|
|
87599
|
+
const offset = loadTurnOffsets(path3).get(sessionId);
|
|
87600
|
+
const movedTo = loadTurnMoves(path3).get(sessionId);
|
|
87601
|
+
if (offset === void 0 && movedTo === void 0) return null;
|
|
87602
|
+
return { uuid: loadTurnCursors(path3).get(sessionId) ?? "", offset: offset ?? 0, ...movedTo ? { movedTo } : {} };
|
|
87603
|
+
}
|
|
87604
|
+
function releaseTurnCursorOnDisk(configDir, sessionId, to) {
|
|
87605
|
+
const path3 = turnCursorFile(configDir);
|
|
87606
|
+
const moves = loadTurnMoves(path3);
|
|
87607
|
+
moves.set(sessionId, to);
|
|
87608
|
+
saveTurnCursors(path3, loadTurnCursors(path3), loadTurnOffsets(path3), moves);
|
|
87609
|
+
}
|
|
87535
87610
|
function gitSourceWatchTargets(root) {
|
|
87536
87611
|
const SOURCE = /\.(ts|tsx|js|jsx|mjs|cjs|py)$/i;
|
|
87537
87612
|
try {
|
|
@@ -88538,9 +88613,10 @@ function createWorkspaceEngine(opts) {
|
|
|
88538
88613
|
resultSummary: { exitCode: e.exitCode ?? 1, errorTokens: e.errorTokens }
|
|
88539
88614
|
});
|
|
88540
88615
|
};
|
|
88541
|
-
const turnCursorPath =
|
|
88616
|
+
const turnCursorPath = turnCursorFile(paths.configDir);
|
|
88542
88617
|
const lastTurnUuid = loadTurnCursors(turnCursorPath);
|
|
88543
88618
|
const turnOffset = loadTurnOffsets(turnCursorPath);
|
|
88619
|
+
const turnMovedTo = loadTurnMoves(turnCursorPath);
|
|
88544
88620
|
const readLosses = { parseFailures: 0, ioFailures: 0, bytesUnreachable: 0, sliceGuardHits: 0 };
|
|
88545
88621
|
const sessionLastProblem = /* @__PURE__ */ new Map();
|
|
88546
88622
|
const sessionThreads = /* @__PURE__ */ new Map();
|
|
@@ -89548,7 +89624,7 @@ function createWorkspaceEngine(opts) {
|
|
|
89548
89624
|
lastTurnUuid.set(sessionId, turns[turns.length - 1].uuid);
|
|
89549
89625
|
await harvestTexts(sessionId, fresh);
|
|
89550
89626
|
}
|
|
89551
|
-
saveTurnCursors(turnCursorPath, lastTurnUuid, turnOffset);
|
|
89627
|
+
saveTurnCursors(turnCursorPath, lastTurnUuid, turnOffset, turnMovedTo);
|
|
89552
89628
|
if (slice >= 256) {
|
|
89553
89629
|
readLosses.sliceGuardHits++;
|
|
89554
89630
|
console.warn("[errata] harvest slice guard hit \u2014 backlog resumes next tick");
|
|
@@ -89565,7 +89641,7 @@ function createWorkspaceEngine(opts) {
|
|
|
89565
89641
|
if (next.losses.ioFailed) readLosses.ioFailures++;
|
|
89566
89642
|
if (next.turns.length === 0 && !next.more) {
|
|
89567
89643
|
turnOffset.set(sessionId, next.nextOffset);
|
|
89568
|
-
saveTurnCursors(turnCursorPath, lastTurnUuid, turnOffset);
|
|
89644
|
+
saveTurnCursors(turnCursorPath, lastTurnUuid, turnOffset, turnMovedTo);
|
|
89569
89645
|
break;
|
|
89570
89646
|
}
|
|
89571
89647
|
turns = next.turns;
|
|
@@ -89619,6 +89695,7 @@ function createWorkspaceEngine(opts) {
|
|
|
89619
89695
|
}
|
|
89620
89696
|
};
|
|
89621
89697
|
const harvestSession = async (sessionId, transcriptPath) => {
|
|
89698
|
+
if (turnMovedTo.has(sessionId)) return;
|
|
89622
89699
|
await harvestTurns(sessionId, transcriptPath);
|
|
89623
89700
|
for (const sub of subagentTranscripts(transcriptPath, sessionId)) {
|
|
89624
89701
|
await yieldToLoop();
|
|
@@ -89854,6 +89931,28 @@ B: ${b}`,
|
|
|
89854
89931
|
storeReconciled: () => storeReconciled,
|
|
89855
89932
|
lastGitEventAt: () => lastGitEventTs,
|
|
89856
89933
|
quarantinedSessions: () => sessions.originSessions(),
|
|
89934
|
+
turnCursor(sessionId) {
|
|
89935
|
+
const offset = turnOffset.get(sessionId);
|
|
89936
|
+
const movedTo = turnMovedTo.get(sessionId);
|
|
89937
|
+
if (offset === void 0 && movedTo === void 0) return null;
|
|
89938
|
+
return { uuid: lastTurnUuid.get(sessionId) ?? "", offset: offset ?? 0, ...movedTo ? { movedTo } : {} };
|
|
89939
|
+
},
|
|
89940
|
+
releaseTurnCursor(sessionId, to) {
|
|
89941
|
+
turnMovedTo.set(sessionId, to);
|
|
89942
|
+
saveTurnCursors(turnCursorPath, lastTurnUuid, turnOffset, turnMovedTo);
|
|
89943
|
+
},
|
|
89944
|
+
adoptTurnCursor(sessionId, cursor) {
|
|
89945
|
+
turnMovedTo.delete(sessionId);
|
|
89946
|
+
if (cursor && cursor.offset > (turnOffset.get(sessionId) ?? -1)) {
|
|
89947
|
+
turnOffset.set(sessionId, cursor.offset);
|
|
89948
|
+
if (cursor.uuid) lastTurnUuid.set(sessionId, cursor.uuid);
|
|
89949
|
+
}
|
|
89950
|
+
saveTurnCursors(turnCursorPath, lastTurnUuid, turnOffset, turnMovedTo);
|
|
89951
|
+
},
|
|
89952
|
+
async settleTurns(sessionId) {
|
|
89953
|
+
await (harvestInFlight.get(sessionId) ?? Promise.resolve()).catch(() => {
|
|
89954
|
+
});
|
|
89955
|
+
},
|
|
89857
89956
|
markContextDirty() {
|
|
89858
89957
|
contextDirty = true;
|
|
89859
89958
|
refreshContextNow();
|
|
@@ -90903,7 +91002,7 @@ function invalidateInstanceReceipt(current, destination, failedId, mirrorLegacy)
|
|
|
90903
91002
|
init_dist();
|
|
90904
91003
|
init_src5();
|
|
90905
91004
|
import { existsSync as existsSync34, readFileSync as readFileSync40, writeFileSync as writeFileSync29 } from "node:fs";
|
|
90906
|
-
import { dirname as dirname17, join as join47, resolve as pathResolve } from "node:path";
|
|
91005
|
+
import { basename as basename10, dirname as dirname17, join as join47, resolve as pathResolve } from "node:path";
|
|
90907
91006
|
|
|
90908
91007
|
// src/principle-sync.ts
|
|
90909
91008
|
init_src5();
|
|
@@ -95385,6 +95484,15 @@ function absolutePathsInCommand(command, platform = process.platform) {
|
|
|
95385
95484
|
}
|
|
95386
95485
|
return out2;
|
|
95387
95486
|
}
|
|
95487
|
+
function leadingCdTarget(command, platform = process.platform) {
|
|
95488
|
+
if (typeof command !== "string") return void 0;
|
|
95489
|
+
const m = /^\s*(?:cd|pushd|Set-Location)\s+("[^"]+"|'[^']+'|[^\s;&|]+)\s*(?:&&|;|\|\||$)/.exec(command);
|
|
95490
|
+
if (!m) return void 0;
|
|
95491
|
+
const target = m[1].replace(/^["']|["']$/g, "");
|
|
95492
|
+
if (/^[a-zA-Z]:[\\/]/.test(target)) return target;
|
|
95493
|
+
if (platform === "win32") return /^\/[a-zA-Z](\/|$)/.test(target) ? msysToWindowsPath(target, platform) : void 0;
|
|
95494
|
+
return target.startsWith("/") ? target : void 0;
|
|
95495
|
+
}
|
|
95388
95496
|
function hookPathCandidates(body2, platform = process.platform) {
|
|
95389
95497
|
const input = body2["tool_input"] ?? {};
|
|
95390
95498
|
const str = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
@@ -95430,6 +95538,205 @@ var SessionAffinity = class {
|
|
|
95430
95538
|
if (typeof sessionId === "string") this.bySession.delete(sessionId);
|
|
95431
95539
|
}
|
|
95432
95540
|
};
|
|
95541
|
+
var FOCUS_WINDOW = 5;
|
|
95542
|
+
var FOCUS_ROOT_HEADER = "x-errata-focus-root";
|
|
95543
|
+
var FOCUS_QUORUM = 3;
|
|
95544
|
+
var FOCUS_TTL_MS = 6 * 60 * 6e4;
|
|
95545
|
+
var SessionFocus = class {
|
|
95546
|
+
constructor(ttlMs = FOCUS_TTL_MS, maxSessions = 500) {
|
|
95547
|
+
this.ttlMs = ttlMs;
|
|
95548
|
+
this.maxSessions = maxSessions;
|
|
95549
|
+
}
|
|
95550
|
+
bySession = /* @__PURE__ */ new Map();
|
|
95551
|
+
live(sessionId, now) {
|
|
95552
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
95553
|
+
const s = this.bySession.get(sessionId);
|
|
95554
|
+
if (!s) return null;
|
|
95555
|
+
if (now - s.at > this.ttlMs) {
|
|
95556
|
+
this.bySession.delete(sessionId);
|
|
95557
|
+
return null;
|
|
95558
|
+
}
|
|
95559
|
+
return s;
|
|
95560
|
+
}
|
|
95561
|
+
touch(sessionId, s, now) {
|
|
95562
|
+
s.at = now;
|
|
95563
|
+
this.bySession.delete(sessionId);
|
|
95564
|
+
this.bySession.set(sessionId, s);
|
|
95565
|
+
while (this.bySession.size > this.maxSessions) {
|
|
95566
|
+
const oldest = this.bySession.keys().next().value;
|
|
95567
|
+
if (oldest === void 0) break;
|
|
95568
|
+
this.bySession.delete(oldest);
|
|
95569
|
+
}
|
|
95570
|
+
}
|
|
95571
|
+
/** The workspace this session's hooks should go to instead of its cwd's, if any. */
|
|
95572
|
+
focusFor(sessionId, now = Date.now()) {
|
|
95573
|
+
return this.live(sessionId, now)?.focus ?? null;
|
|
95574
|
+
}
|
|
95575
|
+
pinnedFor(sessionId, now = Date.now()) {
|
|
95576
|
+
return this.live(sessionId, now)?.pinned ?? false;
|
|
95577
|
+
}
|
|
95578
|
+
/** A tool hook's locator reached `root`; `cwdRoot` is where the session's cwd routes. */
|
|
95579
|
+
observe(sessionId, root, cwdRoot, now = Date.now()) {
|
|
95580
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
95581
|
+
const s = this.live(sessionId, now) ?? { recent: [], focus: null, pinned: false, at: now, reached: /* @__PURE__ */ new Set() };
|
|
95582
|
+
s.reached.add(root);
|
|
95583
|
+
s.recent.push(root);
|
|
95584
|
+
if (s.recent.length > FOCUS_WINDOW) s.recent.shift();
|
|
95585
|
+
this.touch(sessionId, s, now);
|
|
95586
|
+
if (s.pinned) return null;
|
|
95587
|
+
const current = s.focus ?? cwdRoot;
|
|
95588
|
+
if (root === current) return null;
|
|
95589
|
+
if (s.recent.filter((r) => r === root).length < FOCUS_QUORUM) return null;
|
|
95590
|
+
s.focus = root === cwdRoot ? null : root;
|
|
95591
|
+
return { from: current, to: root, reason: "inferred" };
|
|
95592
|
+
}
|
|
95593
|
+
pin(sessionId, root, cwdRoot, now = Date.now()) {
|
|
95594
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
95595
|
+
const s = this.live(sessionId, now) ?? { recent: [], focus: null, pinned: false, at: now, reached: /* @__PURE__ */ new Set() };
|
|
95596
|
+
const current = s.focus ?? cwdRoot;
|
|
95597
|
+
s.pinned = true;
|
|
95598
|
+
s.focus = root;
|
|
95599
|
+
s.recent = [];
|
|
95600
|
+
this.touch(sessionId, s, now);
|
|
95601
|
+
return root === current ? null : { from: current, to: root, reason: "pinned" };
|
|
95602
|
+
}
|
|
95603
|
+
/** Drop the pin and any inferred focus: the session follows its cwd again. */
|
|
95604
|
+
unpin(sessionId, cwdRoot, now = Date.now()) {
|
|
95605
|
+
const s = this.live(sessionId, now);
|
|
95606
|
+
if (!s) return null;
|
|
95607
|
+
const current = s.focus ?? cwdRoot;
|
|
95608
|
+
s.pinned = false;
|
|
95609
|
+
s.focus = null;
|
|
95610
|
+
s.recent = [];
|
|
95611
|
+
this.touch(sessionId, s, now);
|
|
95612
|
+
return cwdRoot && current !== cwdRoot ? { from: current, to: cwdRoot, reason: "unpinned" } : null;
|
|
95613
|
+
}
|
|
95614
|
+
/** A tool ran in the session's cwd without naming a path (a shell command, a search). */
|
|
95615
|
+
ranIn(sessionId, root, now = Date.now()) {
|
|
95616
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return;
|
|
95617
|
+
const s = this.live(sessionId, now) ?? { recent: [], focus: null, pinned: false, at: now, reached: /* @__PURE__ */ new Set() };
|
|
95618
|
+
s.reached.add(root);
|
|
95619
|
+
this.touch(sessionId, s, now);
|
|
95620
|
+
}
|
|
95621
|
+
/** Whether the session's tools ever worked in `root`; undefined for a session this daemon has not watched. */
|
|
95622
|
+
reached(sessionId, root, now = Date.now()) {
|
|
95623
|
+
return this.live(sessionId, now)?.reached.has(root);
|
|
95624
|
+
}
|
|
95625
|
+
forget(sessionId) {
|
|
95626
|
+
if (typeof sessionId === "string") this.bySession.delete(sessionId);
|
|
95627
|
+
}
|
|
95628
|
+
/** Every live focus, for `errata focus` to confirm a pin landed. */
|
|
95629
|
+
entries(now = Date.now()) {
|
|
95630
|
+
const out2 = [];
|
|
95631
|
+
for (const [sessionId, s] of this.bySession) {
|
|
95632
|
+
if (now - s.at > this.ttlMs || !s.focus) continue;
|
|
95633
|
+
out2.push({ sessionId, root: s.focus, pinned: s.pinned, at: s.at });
|
|
95634
|
+
}
|
|
95635
|
+
return out2;
|
|
95636
|
+
}
|
|
95637
|
+
};
|
|
95638
|
+
function misfiledTarget(roots, home) {
|
|
95639
|
+
if (roots.includes(home)) return null;
|
|
95640
|
+
const tally = /* @__PURE__ */ new Map();
|
|
95641
|
+
for (const r of roots) tally.set(r, (tally.get(r) ?? 0) + 1);
|
|
95642
|
+
const ranked = [...tally].sort((a, b) => b[1] - a[1]);
|
|
95643
|
+
const [top, next] = ranked;
|
|
95644
|
+
if (!top || top[1] < FOCUS_QUORUM || next && next[1] === top[1]) return null;
|
|
95645
|
+
return top[0];
|
|
95646
|
+
}
|
|
95647
|
+
function focusCommandIn(command) {
|
|
95648
|
+
if (typeof command !== "string") return void 0;
|
|
95649
|
+
const m = /(?:^|[\s;&|(])errata(?:\.cmd|\.ps1)?\s+focus(?:\s+("[^"]+"|'[^']+'|[^\s;&|)]+))?/.exec(command);
|
|
95650
|
+
if (!m) return void 0;
|
|
95651
|
+
const arg = m[1]?.replace(/^["']|["']$/g, "");
|
|
95652
|
+
if (!arg) return void 0;
|
|
95653
|
+
if (arg === "--clear" || arg === "clear" || arg === "off") return { ref: null };
|
|
95654
|
+
return arg.startsWith("-") ? void 0 : { ref: arg };
|
|
95655
|
+
}
|
|
95656
|
+
function resolveWorkspaceRef(ref, entries, ownerRoot) {
|
|
95657
|
+
const byName = entries.filter((e) => e.name.toLowerCase() === ref.toLowerCase());
|
|
95658
|
+
if (byName.length === 1) return byName[0];
|
|
95659
|
+
if (byName.length > 1) return null;
|
|
95660
|
+
const root = ownerRoot(entries.map((e) => e.path), msysToWindowsPath(ref));
|
|
95661
|
+
return root ? entries.find((e) => e.path === root) ?? null : null;
|
|
95662
|
+
}
|
|
95663
|
+
|
|
95664
|
+
// src/rehome.ts
|
|
95665
|
+
init_src2();
|
|
95666
|
+
import { isAbsolute as isAbsolute6 } from "node:path";
|
|
95667
|
+
function sourcesOf(n) {
|
|
95668
|
+
const s = n.attrs["sources"];
|
|
95669
|
+
return Array.isArray(s) ? s.filter((x) => typeof x === "string") : [];
|
|
95670
|
+
}
|
|
95671
|
+
function rehomeSessionNodes(from, to, sessionIds, opts) {
|
|
95672
|
+
const sessions = new Set(sessionIds);
|
|
95673
|
+
const now = opts.now ?? Date.now();
|
|
95674
|
+
const report = { moved: [], copied: [], vocabulary: [], edgesCarried: 0, edgesDropped: 0, cloudContributed: [] };
|
|
95675
|
+
const picked = /* @__PURE__ */ new Map();
|
|
95676
|
+
for (const row of from.selectNodes({ labels: SEMANTIC_NODE_LABELS, attrs: ["sources"] })) {
|
|
95677
|
+
const sources = sourcesOf(row);
|
|
95678
|
+
if (!sources.some((s) => sessions.has(s))) continue;
|
|
95679
|
+
const node2 = from.getNode(row.id);
|
|
95680
|
+
if (!node2) continue;
|
|
95681
|
+
picked.set(node2.id, node2);
|
|
95682
|
+
(sources.every((s) => sessions.has(s)) ? report.moved : report.copied).push(node2.id);
|
|
95683
|
+
if (node2.attrs["cloudNodeId"]) report.cloudContributed.push(node2.id);
|
|
95684
|
+
}
|
|
95685
|
+
const edges = /* @__PURE__ */ new Map();
|
|
95686
|
+
for (const id of picked.keys()) {
|
|
95687
|
+
for (const e of [...from.outEdges(id), ...from.inEdges(id)]) edges.set(e.id, e);
|
|
95688
|
+
}
|
|
95689
|
+
const vocabulary = /* @__PURE__ */ new Map();
|
|
95690
|
+
const carried = [];
|
|
95691
|
+
for (const e of edges.values()) {
|
|
95692
|
+
const other = picked.has(e.from) ? e.to : e.from;
|
|
95693
|
+
if (picked.has(other)) {
|
|
95694
|
+
carried.push(e);
|
|
95695
|
+
continue;
|
|
95696
|
+
}
|
|
95697
|
+
const node2 = from.getNode(other);
|
|
95698
|
+
if (!node2 || isCodeLabel(node2.label)) {
|
|
95699
|
+
report.edgesDropped++;
|
|
95700
|
+
continue;
|
|
95701
|
+
}
|
|
95702
|
+
if (!to.getNode(other)) vocabulary.set(other, node2);
|
|
95703
|
+
carried.push(e);
|
|
95704
|
+
}
|
|
95705
|
+
report.vocabulary = [...vocabulary.keys()];
|
|
95706
|
+
report.edgesCarried = carried.length;
|
|
95707
|
+
if (!opts.apply) return report;
|
|
95708
|
+
to.transaction(() => {
|
|
95709
|
+
for (const n of [...vocabulary.values(), ...picked.values()]) to.mergeNode(n);
|
|
95710
|
+
for (const e of carried) to.mergeEdge(e);
|
|
95711
|
+
});
|
|
95712
|
+
from.transaction(() => {
|
|
95713
|
+
for (const id of report.moved) {
|
|
95714
|
+
const n = picked.get(id);
|
|
95715
|
+
from.updateNode(id, { attrs: { ...n.attrs, rehomedAt: now } });
|
|
95716
|
+
from.closeNode(id, now);
|
|
95717
|
+
}
|
|
95718
|
+
});
|
|
95719
|
+
return report;
|
|
95720
|
+
}
|
|
95721
|
+
function captureWorkPaths(store, home) {
|
|
95722
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
95723
|
+
for (const row of store.selectNodes({ labels: ["Problem"], attrs: ["sources", "anchorHint"] })) {
|
|
95724
|
+
const sources = sourcesOf(row);
|
|
95725
|
+
if (sources.length === 0) continue;
|
|
95726
|
+
let where = null;
|
|
95727
|
+
if (store.outEdges(row.id, ["ANCHORED_AT"]).length > 0) where = home;
|
|
95728
|
+
else {
|
|
95729
|
+
const hint = row.attrs["anchorHint"];
|
|
95730
|
+
if (typeof hint?.path === "string" && hint.path.length > 0) where = isAbsolute6(hint.path) ? hint.path : home;
|
|
95731
|
+
}
|
|
95732
|
+
for (const s of sources) {
|
|
95733
|
+
const list = out2.get(s) ?? [];
|
|
95734
|
+
if (where) list.push(where);
|
|
95735
|
+
out2.set(s, list);
|
|
95736
|
+
}
|
|
95737
|
+
}
|
|
95738
|
+
return out2;
|
|
95739
|
+
}
|
|
95433
95740
|
|
|
95434
95741
|
// src/multi.ts
|
|
95435
95742
|
init_checkout_root();
|
|
@@ -95487,6 +95794,14 @@ function pickOwnerRoot(roots, absPath) {
|
|
|
95487
95794
|
}
|
|
95488
95795
|
return best;
|
|
95489
95796
|
}
|
|
95797
|
+
function withAdditionalContext(response, hookEventName, extra) {
|
|
95798
|
+
const out2 = response["hookSpecificOutput"] ?? {};
|
|
95799
|
+
const prior = typeof out2.additionalContext === "string" && out2.additionalContext ? `${out2.additionalContext}
|
|
95800
|
+
|
|
95801
|
+
` : "";
|
|
95802
|
+
const { ok: _ok, event: _event, tool: _tool, recognizedFile: _file2, ...rest2 } = response;
|
|
95803
|
+
return { ...rest2, hookSpecificOutput: { ...out2, hookEventName, additionalContext: `${prior}${extra}` } };
|
|
95804
|
+
}
|
|
95490
95805
|
function ownerOf(records, absPath) {
|
|
95491
95806
|
const root = pickOwnerRoot(records.map((r) => r.root), absPath);
|
|
95492
95807
|
return root == null ? null : records.find((r) => r.root === root) ?? null;
|
|
@@ -95893,6 +96208,8 @@ async function startMultiDaemon(opts = {}) {
|
|
|
95893
96208
|
const app = new Hono2();
|
|
95894
96209
|
const enqueueWakeIndex = serialQueue();
|
|
95895
96210
|
const attaching = /* @__PURE__ */ new Map();
|
|
96211
|
+
let afterAttach = () => {
|
|
96212
|
+
};
|
|
95896
96213
|
const attachWorkspace = (root) => {
|
|
95897
96214
|
const norm = normPath(root);
|
|
95898
96215
|
const pending = attaching.get(norm);
|
|
@@ -95942,6 +96259,7 @@ async function startMultiDaemon(opts = {}) {
|
|
|
95942
96259
|
});
|
|
95943
96260
|
}
|
|
95944
96261
|
console.log(`[errata] hot-registered ${rec.entry.name} (${rec.id}) \u2014 now watching live`);
|
|
96262
|
+
afterAttach(rec);
|
|
95945
96263
|
return { attached: true, id: rec.id };
|
|
95946
96264
|
};
|
|
95947
96265
|
app.post("/api/attach", async (c) => {
|
|
@@ -96066,6 +96384,105 @@ async function startMultiDaemon(opts = {}) {
|
|
|
96066
96384
|
})
|
|
96067
96385
|
);
|
|
96068
96386
|
const sessionAffinity = new SessionAffinity();
|
|
96387
|
+
const sessionFocus = new SessionFocus();
|
|
96388
|
+
const pendingSwitch = /* @__PURE__ */ new Map();
|
|
96389
|
+
const turnHome = /* @__PURE__ */ new Map();
|
|
96390
|
+
const registryRoots = () => listWorkspaceEntries().map((e) => e.path);
|
|
96391
|
+
const rootOfPath = (p, roots) => p ? ownerOf(records, p)?.root ?? pickOwnerRoot((roots ?? registryRoots)(), p) : null;
|
|
96392
|
+
const workspaceName = (root) => root ? listWorkspaceEntries().find((e) => e.path === root)?.name ?? basename10(root) : "its working directory";
|
|
96393
|
+
const recordFor = async (root) => {
|
|
96394
|
+
const running = records.find((r) => r.root === root);
|
|
96395
|
+
if (running) return running;
|
|
96396
|
+
return (await attachWorkspace(root)).attached ? records.find((r) => r.root === root) ?? null : null;
|
|
96397
|
+
};
|
|
96398
|
+
const focusNotice = (change, cwdRoot) => {
|
|
96399
|
+
const to = workspaceName(change.to);
|
|
96400
|
+
const from = workspaceName(change.from);
|
|
96401
|
+
const where = `Captures from here on go to ${to}'s graph, and the block below is ${to}'s context: it replaces any earlier errata block.`;
|
|
96402
|
+
if (change.reason === "pinned") return `errata: this session is now pinned to ${to} (\`errata focus\`). ${where} \`errata focus off\` returns it to ${workspaceName(cwdRoot)}.`;
|
|
96403
|
+
if (change.reason === "unpinned") return `errata: focus cleared; this session follows its working directory, ${to}, again. ${where}`;
|
|
96404
|
+
return `errata: this session's work moved from ${from} to ${to}: ${FOCUS_QUORUM} of the last ${FOCUS_WINDOW} paths your tools touched are in ${to}. ${where} Wrong workspace? Run \`errata focus ${from === "its working directory" ? "off" : from}\`.`;
|
|
96405
|
+
};
|
|
96406
|
+
const noteFocus = (sid, change) => {
|
|
96407
|
+
if (!change) return;
|
|
96408
|
+
pendingSwitch.set(sid, change);
|
|
96409
|
+
console.log(`[errata] session ${sid.slice(0, 8)} focus ${workspaceName(change.from)} \u2192 ${workspaceName(change.to)} (${change.reason})`);
|
|
96410
|
+
};
|
|
96411
|
+
const observeFocus = (sid, body2) => {
|
|
96412
|
+
const event = String(body2["hook_event_name"] ?? "");
|
|
96413
|
+
const cwd = typeof body2["cwd"] === "string" ? msysToWindowsPath(body2["cwd"]) : void 0;
|
|
96414
|
+
const cwdRoot = rootOfPath(cwd);
|
|
96415
|
+
const input = body2["tool_input"] ?? {};
|
|
96416
|
+
if (event === "PreToolUse") {
|
|
96417
|
+
const pin = focusCommandIn(input["command"]);
|
|
96418
|
+
if (!pin) return;
|
|
96419
|
+
if (pin.ref === null) return noteFocus(sid, sessionFocus.unpin(sid, cwdRoot));
|
|
96420
|
+
const entry = resolveWorkspaceRef(pin.ref, listWorkspaceEntries(), pickOwnerRoot);
|
|
96421
|
+
if (entry) noteFocus(sid, sessionFocus.pin(sid, entry.path, cwdRoot));
|
|
96422
|
+
return;
|
|
96423
|
+
}
|
|
96424
|
+
if (event !== "PostToolUse" && event !== "PostToolUseFailure") return;
|
|
96425
|
+
let registered = null;
|
|
96426
|
+
const roots = () => registered ??= registryRoots();
|
|
96427
|
+
const located = hookPathCandidates(body2).filter((p) => p !== cwd).map((p) => rootOfPath(p, roots)).find((r) => r != null);
|
|
96428
|
+
if (located) noteFocus(sid, sessionFocus.observe(sid, located, cwdRoot));
|
|
96429
|
+
else if (cwdRoot && (typeof input["command"] === "string" || ["Grep", "Glob", "LS"].includes(String(body2["tool_name"])))) {
|
|
96430
|
+
sessionFocus.ranIn(sid, cwdRoot);
|
|
96431
|
+
}
|
|
96432
|
+
};
|
|
96433
|
+
const repositoryOf = (root) => {
|
|
96434
|
+
const gitRoot = nearestGitRoot(root);
|
|
96435
|
+
return normPath((gitRoot ? mainCheckoutOfWorktree(gitRoot) : null) ?? root);
|
|
96436
|
+
};
|
|
96437
|
+
const evidenceWorkspace = (home) => {
|
|
96438
|
+
let registered = null;
|
|
96439
|
+
const known = () => registered ??= registryRoots();
|
|
96440
|
+
return (p) => {
|
|
96441
|
+
if (p === home) return p;
|
|
96442
|
+
const direct = rootOfPath(p, known);
|
|
96443
|
+
if (direct) return direct;
|
|
96444
|
+
const gitRoot = nearestGitRoot(p);
|
|
96445
|
+
const main2 = gitRoot ? mainCheckoutOfWorktree(gitRoot) : null;
|
|
96446
|
+
return main2 ? rootOfPath(main2, known) : null;
|
|
96447
|
+
};
|
|
96448
|
+
};
|
|
96449
|
+
const rehomeMisfiled = async (sid, from, to, why) => {
|
|
96450
|
+
if (repositoryOf(from.root) === repositoryOf(to.root)) return;
|
|
96451
|
+
await from.engine.settleTurns(sid);
|
|
96452
|
+
const workspaceOf = evidenceWorkspace(from.root);
|
|
96453
|
+
if ((captureWorkPaths(from.engine.store, from.root).get(sid) ?? []).some((p) => workspaceOf(p) === from.root)) return;
|
|
96454
|
+
const report = rehomeSessionNodes(from.engine.store, to.engine.store, [sid], { apply: true });
|
|
96455
|
+
if (report.moved.length + report.copied.length === 0) return;
|
|
96456
|
+
from.engine.markContextDirty();
|
|
96457
|
+
to.engine.markContextDirty();
|
|
96458
|
+
console.log(
|
|
96459
|
+
`[errata] session ${sid.slice(0, 8)} captures ${from.entry.name} \u2192 ${to.entry.name} (${why}): ${report.moved.length} moved, ${report.copied.length} copied, ${report.edgesDropped} code edges dropped` + (report.cloudContributed.length > 0 ? `, ${report.cloudContributed.length} already in the cloud under ${from.entry.name}` : "")
|
|
96460
|
+
);
|
|
96461
|
+
};
|
|
96462
|
+
const handOffTurns = (sid, owner, cwd) => {
|
|
96463
|
+
const mine = owner.engine.turnCursor(sid);
|
|
96464
|
+
let fromRoot = mine?.movedTo ?? turnHome.get(sid) ?? null;
|
|
96465
|
+
if (!fromRoot && !mine) fromRoot = rootOfPath(cwd);
|
|
96466
|
+
if (fromRoot && fromRoot !== owner.root) {
|
|
96467
|
+
const from = records.find((r) => r.root === fromRoot);
|
|
96468
|
+
const configDir = workspacePaths(fromRoot).configDir;
|
|
96469
|
+
const cursor = from ? from.engine.turnCursor(sid) : turnCursorOnDisk(configDir, sid);
|
|
96470
|
+
if (from) from.engine.releaseTurnCursor(sid, owner.root);
|
|
96471
|
+
else releaseTurnCursorOnDisk(configDir, sid, owner.root);
|
|
96472
|
+
owner.engine.adoptTurnCursor(sid, cursor?.movedTo ? null : cursor);
|
|
96473
|
+
console.log(`[errata] session ${sid.slice(0, 8)} turns: ${workspaceName(fromRoot)} \u2192 ${owner.entry.name}`);
|
|
96474
|
+
if (from && sessionFocus.focusFor(sid) === owner.root && sessionFocus.reached(sid, fromRoot) === false) {
|
|
96475
|
+
void rehomeMisfiled(sid, from, owner, "its tools never worked there").catch(
|
|
96476
|
+
(err2) => console.warn(`[errata] rehome of session ${sid.slice(0, 8)} failed: ${err2 instanceof Error ? err2.message : err2}`)
|
|
96477
|
+
);
|
|
96478
|
+
}
|
|
96479
|
+
} else if (mine?.movedTo) {
|
|
96480
|
+
owner.engine.adoptTurnCursor(sid, null);
|
|
96481
|
+
}
|
|
96482
|
+
turnHome.delete(sid);
|
|
96483
|
+
turnHome.set(sid, owner.root);
|
|
96484
|
+
if (turnHome.size > 500) turnHome.delete(turnHome.keys().next().value);
|
|
96485
|
+
};
|
|
96069
96486
|
const forward = async (c, pathInBody, opts2 = {}) => {
|
|
96070
96487
|
const raw2 = await c.req.text();
|
|
96071
96488
|
let body2 = {};
|
|
@@ -96075,9 +96492,13 @@ async function startMultiDaemon(opts = {}) {
|
|
|
96075
96492
|
return c.json({ error: "bad-json" }, 400);
|
|
96076
96493
|
}
|
|
96077
96494
|
lastHook = { at: Date.now(), event: typeof body2["hook_event_name"] === "string" ? body2["hook_event_name"] : c.req.path };
|
|
96495
|
+
const sid = typeof body2["session_id"] === "string" ? body2["session_id"] : "";
|
|
96496
|
+
if (sid && opts2.affinity === "note") observeFocus(sid, body2);
|
|
96497
|
+
const focusRoot = sid && opts2.affinity === "follow" ? sessionFocus.focusFor(sid) : null;
|
|
96078
96498
|
const primary = pathInBody(body2);
|
|
96079
96499
|
const others = (opts2.extraPaths?.(body2) ?? []).filter((p) => p !== primary);
|
|
96080
|
-
let owner =
|
|
96500
|
+
let owner = focusRoot ? await recordFor(focusRoot) : null;
|
|
96501
|
+
if (!owner) owner = ownerOf(records, primary);
|
|
96081
96502
|
for (const p of others) {
|
|
96082
96503
|
if (owner) break;
|
|
96083
96504
|
owner = ownerOf(records, p);
|
|
@@ -96096,8 +96517,8 @@ async function startMultiDaemon(opts = {}) {
|
|
|
96096
96517
|
}
|
|
96097
96518
|
}
|
|
96098
96519
|
if (!owner && others.length > 0) {
|
|
96099
|
-
const
|
|
96100
|
-
const registered = others.map((p) => pickOwnerRoot(
|
|
96520
|
+
const registryRoots2 = listWorkspaceEntries().map((e) => e.path);
|
|
96521
|
+
const registered = others.map((p) => pickOwnerRoot(registryRoots2, p)).find((r) => r != null);
|
|
96101
96522
|
if (registered && (await attachWorkspace(registered)).attached) {
|
|
96102
96523
|
owner = records.find((r) => r.root === registered) ?? null;
|
|
96103
96524
|
if (owner) console.log(`[errata] woke workspace ${owner.entry.name}`);
|
|
@@ -96126,32 +96547,109 @@ async function startMultiDaemon(opts = {}) {
|
|
|
96126
96547
|
if (opts2.affinity === "note") sessionAffinity.note(body2["session_id"], owner.root);
|
|
96127
96548
|
owner.lastHookAt = Date.now();
|
|
96128
96549
|
const sub = c.req.path;
|
|
96550
|
+
const cwd = typeof body2["cwd"] === "string" ? msysToWindowsPath(body2["cwd"]) : void 0;
|
|
96551
|
+
if (sid && (sub === "/api/turn" || sub === "/api/session-end")) handOffTurns(sid, owner, cwd);
|
|
96552
|
+
const subagentHook = typeof body2["agent_id"] === "string" && body2["agent_id"] !== "";
|
|
96553
|
+
const pending = sid && !subagentHook ? pendingSwitch.get(sid) : void 0;
|
|
96554
|
+
let raw22 = raw2;
|
|
96555
|
+
if (pending && sub === "/api/context-inject" && pending.to === owner.root) {
|
|
96556
|
+
raw22 = JSON.stringify({ ...body2, [FOCUS_NOTICE_FIELD]: focusNotice(pending, rootOfPath(cwd)) });
|
|
96557
|
+
pendingSwitch.delete(sid);
|
|
96558
|
+
}
|
|
96129
96559
|
const tag = (v) => typeof v === "string" ? ` ${v.slice(0, 40)}` : "";
|
|
96130
96560
|
const done = markPass(`${sub}:${owner.entry.name}${tag(body2["hook_event_name"])}${tag(body2["tool_name"])}`);
|
|
96561
|
+
const post = async (target, path3, payload) => await target.webApp.fetch(
|
|
96562
|
+
new Request(`http://local${path3}`, {
|
|
96563
|
+
method: "POST",
|
|
96564
|
+
// The origin header (harness/fixture quarantine, session-registry.ts)
|
|
96565
|
+
// is the one caller header the sub-app needs; nothing else crosses.
|
|
96566
|
+
headers: {
|
|
96567
|
+
"content-type": "application/json",
|
|
96568
|
+
...c.req.header(ORIGIN_HEADER) ? { [ORIGIN_HEADER]: c.req.header(ORIGIN_HEADER) } : {}
|
|
96569
|
+
},
|
|
96570
|
+
body: payload
|
|
96571
|
+
})
|
|
96572
|
+
);
|
|
96573
|
+
const withFocusHeader = (res) => {
|
|
96574
|
+
const focusedRoot = sid ? sessionFocus.focusFor(sid) : null;
|
|
96575
|
+
if (!focusedRoot) return res;
|
|
96576
|
+
const headers = new Headers(res.headers);
|
|
96577
|
+
headers.set(FOCUS_ROOT_HEADER, focusedRoot);
|
|
96578
|
+
return new Response(res.body, { status: res.status, headers });
|
|
96579
|
+
};
|
|
96131
96580
|
try {
|
|
96132
|
-
|
|
96133
|
-
|
|
96134
|
-
|
|
96135
|
-
|
|
96136
|
-
|
|
96137
|
-
|
|
96138
|
-
|
|
96139
|
-
|
|
96140
|
-
|
|
96141
|
-
|
|
96142
|
-
|
|
96143
|
-
|
|
96581
|
+
const res = await post(owner, sub, raw22);
|
|
96582
|
+
if (sub === "/api/session-end" && sid) {
|
|
96583
|
+
sessionFocus.forget(sid);
|
|
96584
|
+
pendingSwitch.delete(sid);
|
|
96585
|
+
}
|
|
96586
|
+
const hookEvent = String(body2["hook_event_name"] ?? "");
|
|
96587
|
+
if (pending && pendingSwitch.has(sid) && sub === "/api/hook" && ["PreToolUse", "PostToolUse", "PostToolUseFailure"].includes(hookEvent)) {
|
|
96588
|
+
const target = await recordFor(pending.to);
|
|
96589
|
+
if (target) {
|
|
96590
|
+
pendingSwitch.delete(sid);
|
|
96591
|
+
const inject = await post(target, "/api/context-inject", JSON.stringify({
|
|
96592
|
+
session_id: sid,
|
|
96593
|
+
hook_event_name: hookEvent,
|
|
96594
|
+
cwd: target.root,
|
|
96595
|
+
...typeof body2["transcript_path"] === "string" ? { transcript_path: body2["transcript_path"] } : {},
|
|
96596
|
+
[FOCUS_NOTICE_FIELD]: focusNotice(pending, rootOfPath(cwd))
|
|
96597
|
+
}));
|
|
96598
|
+
const extra = (await inject.json().catch(() => ({}))).hookSpecificOutput?.additionalContext;
|
|
96599
|
+
if (extra) {
|
|
96600
|
+
const base = res.status === 204 ? {} : await res.json().catch(() => ({}));
|
|
96601
|
+
return withFocusHeader(c.json(withAdditionalContext(base, hookEvent, extra)));
|
|
96602
|
+
}
|
|
96603
|
+
}
|
|
96604
|
+
}
|
|
96605
|
+
return withFocusHeader(res);
|
|
96144
96606
|
} finally {
|
|
96145
96607
|
done();
|
|
96146
96608
|
}
|
|
96147
96609
|
};
|
|
96610
|
+
const MISFILED_SWEEP_DELAY_MS = 2 * 6e4;
|
|
96611
|
+
const sweepMisfiled = async (rec) => {
|
|
96612
|
+
const checkedPath = join47(workspacePaths(rec.root).configDir, "rehome-checked.json");
|
|
96613
|
+
let checked;
|
|
96614
|
+
try {
|
|
96615
|
+
checked = new Set(JSON.parse(readFileSync40(checkedPath, "utf8")));
|
|
96616
|
+
} catch {
|
|
96617
|
+
checked = /* @__PURE__ */ new Set();
|
|
96618
|
+
}
|
|
96619
|
+
const workspaceOf = evidenceWorkspace(rec.root);
|
|
96620
|
+
let changed = false;
|
|
96621
|
+
for (const [sid, paths] of captureWorkPaths(rec.engine.store, rec.root)) {
|
|
96622
|
+
if (stopping) break;
|
|
96623
|
+
if (checked.has(sid) || turnHome.has(sid) || sessionFocus.reached(sid, rec.root) !== void 0) continue;
|
|
96624
|
+
checked.add(sid);
|
|
96625
|
+
changed = true;
|
|
96626
|
+
const target = misfiledTarget(paths.map(workspaceOf).filter((r) => r != null), rec.root);
|
|
96627
|
+
const to = target ? await recordFor(target) : null;
|
|
96628
|
+
if (to) await rehomeMisfiled(sid, rec, to, "its captures were raised in its files");
|
|
96629
|
+
await new Promise((r) => setImmediate(r));
|
|
96630
|
+
}
|
|
96631
|
+
if (changed) writeFileSync29(checkedPath, JSON.stringify([...checked]), "utf8");
|
|
96632
|
+
};
|
|
96633
|
+
afterAttach = (rec) => {
|
|
96634
|
+
const t = setTimeout(() => {
|
|
96635
|
+
void sweepMisfiled(rec).catch(
|
|
96636
|
+
(err2) => console.warn(`[errata] misfiled-session check of ${rec.entry.name} failed: ${err2 instanceof Error ? err2.message : err2}`)
|
|
96637
|
+
);
|
|
96638
|
+
}, MISFILED_SWEEP_DELAY_MS);
|
|
96639
|
+
t.unref?.();
|
|
96640
|
+
};
|
|
96641
|
+
for (const rec of records) afterAttach(rec);
|
|
96642
|
+
app.get(
|
|
96643
|
+
"/api/focus",
|
|
96644
|
+
(c) => c.json({ sessions: sessionFocus.entries().map((f) => ({ ...f, name: workspaceName(f.root) })) })
|
|
96645
|
+
);
|
|
96148
96646
|
app.post(
|
|
96149
96647
|
"/api/hook",
|
|
96150
96648
|
(c) => forward(c, (b) => {
|
|
96151
96649
|
const input = b["tool_input"] ?? {};
|
|
96152
96650
|
const fp = input["file_path"];
|
|
96153
96651
|
const cwd = b["cwd"];
|
|
96154
|
-
return (typeof fp === "string" ? fp : void 0) ?? (typeof cwd === "string" ? cwd : void 0);
|
|
96652
|
+
return (typeof fp === "string" ? fp : void 0) ?? leadingCdTarget(input["command"]) ?? (typeof cwd === "string" ? cwd : void 0);
|
|
96155
96653
|
}, { extraPaths: (b) => hookPathCandidates(b), affinity: "note" })
|
|
96156
96654
|
);
|
|
96157
96655
|
app.post(
|
|
@@ -98328,6 +98826,9 @@ try {
|
|
|
98328
98826
|
// a changed snapshot can go out as only what changed (the daemon renders it).
|
|
98329
98827
|
const bodyPath = statePath && statePath.replace(/\.json$/, '.body.md');
|
|
98330
98828
|
let emittedBody = null;
|
|
98829
|
+
// Set when the daemon routes this session to another workspace (errata focus):
|
|
98830
|
+
// that workspace's block is the current one.
|
|
98831
|
+
let focusRoot = null;
|
|
98331
98832
|
const post = async (endpoint, relay = false) => {
|
|
98332
98833
|
try {
|
|
98333
98834
|
const response = await fetch('http://127.0.0.1:' + port + '/api/' + endpoint, {
|
|
@@ -98335,6 +98836,8 @@ try {
|
|
|
98335
98836
|
body: JSON.stringify(payload), signal: AbortSignal.timeout(2000),
|
|
98336
98837
|
});
|
|
98337
98838
|
if (!response.ok) return false;
|
|
98839
|
+
const focused = response.headers.get('x-errata-focus-root');
|
|
98840
|
+
if (focused) focusRoot = focused;
|
|
98338
98841
|
if (!relay) return true;
|
|
98339
98842
|
const result = await response.json();
|
|
98340
98843
|
const context = result.hookSpecificOutput?.additionalContext;
|
|
@@ -98355,17 +98858,20 @@ try {
|
|
|
98355
98858
|
}
|
|
98356
98859
|
if (workspaceRoot && ['SessionStart', 'UserPromptSubmit', 'PostToolUse'].includes(event)) {
|
|
98357
98860
|
let body = '';
|
|
98358
|
-
|
|
98861
|
+
const blockRoot = focusRoot || workspaceRoot;
|
|
98862
|
+
try { body = boundedRead(join(blockRoot, '.errata', 'context.md'), 128 * 1024).trim(); } catch {}
|
|
98359
98863
|
if (body) {
|
|
98360
98864
|
const digest = hash(body.replace(/\r\n?/g, '\n')).slice(0, 16);
|
|
98865
|
+
// A different workspace's block than the one last sent: a delta between them is noise.
|
|
98866
|
+
const switched = (previous.root || workspaceRoot) !== blockRoot;
|
|
98361
98867
|
// Receipt is local to this graph + session, survives daemon restarts, and
|
|
98362
98868
|
// records stdout emission, not a server's intention to send a response.
|
|
98363
|
-
if (event === 'SessionStart' || previous.hash !== digest) {
|
|
98869
|
+
if (event === 'SessionStart' || switched || previous.hash !== digest) {
|
|
98364
98870
|
const normalized = body.replace(/\r\n?/g, '\n');
|
|
98365
98871
|
// Every SessionStart (startup, resume, compact, clear) sends the whole
|
|
98366
98872
|
// block: the window may no longer hold the last one.
|
|
98367
98873
|
let delta = null;
|
|
98368
|
-
if (event !== 'SessionStart' && previous.hash && bodyPath) {
|
|
98874
|
+
if (event !== 'SessionStart' && !switched && previous.hash && bodyPath) {
|
|
98369
98875
|
let prior = '';
|
|
98370
98876
|
try { prior = boundedRead(bodyPath, 512 * 1024); } catch {}
|
|
98371
98877
|
if (prior && hash(prior).slice(0, 16) === previous.hash) {
|
|
@@ -98383,7 +98889,7 @@ try {
|
|
|
98383
98889
|
// Only bookkeeping moved: nothing to send, but this snapshot is now what the window holds.
|
|
98384
98890
|
try {
|
|
98385
98891
|
mkdirSync(stateDir, { recursive: true });
|
|
98386
|
-
writeFileSync(statePath, JSON.stringify({ hash: digest, at: Date.now() }), { mode: 0o600 });
|
|
98892
|
+
writeFileSync(statePath, JSON.stringify({ hash: digest, at: Date.now(), root: blockRoot }), { mode: 0o600 });
|
|
98387
98893
|
writeFileSync(bodyPath, normalized, { mode: 0o600 });
|
|
98388
98894
|
} catch { /* the next change just goes whole */ }
|
|
98389
98895
|
} else {
|
|
@@ -98392,11 +98898,11 @@ try {
|
|
|
98392
98898
|
: 'Errata loaded the maintained local workspace context. This block REPLACES any earlier errata context block in this conversation. It is the latest saved snapshot; the daemon maintains it as work proceeds.\n\n' + body);
|
|
98393
98899
|
if (!/^(0|off|false|no)$/i.test((process.env.ERRATA_RECALL_CARD || '').trim())) {
|
|
98394
98900
|
try {
|
|
98395
|
-
const card = JSON.parse(boundedRead(join(
|
|
98901
|
+
const card = JSON.parse(boundedRead(join(blockRoot, '.errata', 'context-card.json'), 128 * 1024));
|
|
98396
98902
|
if (card.hash === digest && typeof card.line === 'string' && card.line) messages.push(card.line);
|
|
98397
98903
|
} catch { /* Never show a card for a different snapshot. */ }
|
|
98398
98904
|
}
|
|
98399
|
-
receipt = { hash: digest, at: Date.now() };
|
|
98905
|
+
receipt = { hash: digest, at: Date.now(), root: blockRoot };
|
|
98400
98906
|
emittedBody = normalized;
|
|
98401
98907
|
}
|
|
98402
98908
|
}
|
|
@@ -99922,6 +100428,10 @@ async function main() {
|
|
|
99922
100428
|
return cmdUpdate(rest);
|
|
99923
100429
|
case "projects":
|
|
99924
100430
|
return cmdProjects();
|
|
100431
|
+
case "focus":
|
|
100432
|
+
return cmdFocus(rest);
|
|
100433
|
+
case "rehome":
|
|
100434
|
+
return cmdRehome(rest);
|
|
99925
100435
|
case "dash":
|
|
99926
100436
|
return cmdDash(rest);
|
|
99927
100437
|
case "notify-test":
|
|
@@ -99945,6 +100455,14 @@ function printHelp() {
|
|
|
99945
100455
|
Commands:
|
|
99946
100456
|
init Auto-detect this workspace's profile, write .errata/workspace.json
|
|
99947
100457
|
projects List every initialized project + whether its daemon is running
|
|
100458
|
+
focus <workspace|off>
|
|
100459
|
+
Run from an agent's shell tool: that session's captures and
|
|
100460
|
+
context follow the named workspace (name or path). Sessions
|
|
100461
|
+
also follow on their own once most of the paths their tools
|
|
100462
|
+
touch are in another workspace; this pins it.
|
|
100463
|
+
rehome --session <id> --from <ws> --to <ws> [--apply]
|
|
100464
|
+
Move what a session captured into the workspace its work was
|
|
100465
|
+
about (a session started in the wrong repo). Dry run by default.
|
|
99948
100466
|
dash [--port N] [--no-reindex] [--embed]
|
|
99949
100467
|
One daemon watching ALL initialized projects, routing hooks
|
|
99950
100468
|
to each project's graph (default port 7891). JSON endpoints
|
|
@@ -103006,6 +103524,82 @@ async function cmdProjects() {
|
|
|
103006
103524
|
console.log(` ${w.path}`);
|
|
103007
103525
|
}
|
|
103008
103526
|
}
|
|
103527
|
+
async function cmdFocus(args2) {
|
|
103528
|
+
const entries = listWorkspaceEntries();
|
|
103529
|
+
const ref = args2.find((a) => !a.startsWith("-"));
|
|
103530
|
+
const clear = args2.includes("--clear") || ref === "off" || ref === "clear";
|
|
103531
|
+
const port = resolveHookPort();
|
|
103532
|
+
const focused = async () => {
|
|
103533
|
+
try {
|
|
103534
|
+
const r = await fetch(`http://127.0.0.1:${port}/api/focus`, { signal: AbortSignal.timeout(2e3) });
|
|
103535
|
+
return r.ok ? (await r.json()).sessions : [];
|
|
103536
|
+
} catch {
|
|
103537
|
+
return [];
|
|
103538
|
+
}
|
|
103539
|
+
};
|
|
103540
|
+
if (!ref && !clear) {
|
|
103541
|
+
const now = await focused();
|
|
103542
|
+
console.log("usage: errata focus <workspace name or path> | errata focus off");
|
|
103543
|
+
console.log("Run it from an agent's shell tool: the session that runs it follows that workspace.\n");
|
|
103544
|
+
if (now.length === 0) console.log("No session is focused away from its working directory.");
|
|
103545
|
+
for (const f of now) console.log(` ${f.sessionId.slice(0, 8)} ${f.pinned ? "pinned to" : "following"} ${f.name} (${f.root})`);
|
|
103546
|
+
return;
|
|
103547
|
+
}
|
|
103548
|
+
if (clear) {
|
|
103549
|
+
console.log("focus cleared for the agent session that ran this: it follows its working directory again.");
|
|
103550
|
+
return;
|
|
103551
|
+
}
|
|
103552
|
+
const entry = resolveWorkspaceRef(ref, entries, pickOwnerRoot);
|
|
103553
|
+
if (!entry) {
|
|
103554
|
+
const same = entries.filter((e) => e.name.toLowerCase() === ref.toLowerCase());
|
|
103555
|
+
console.error(same.length > 1 ? `"${ref}" names ${same.length} workspaces; use a path instead:
|
|
103556
|
+
${same.map((e) => ` ${e.path}`).join("\n")}` : `no workspace named or containing "${ref}". Known workspaces:
|
|
103557
|
+
${entries.map((e) => ` ${e.name.padEnd(24)} ${e.path}`).join("\n")}`);
|
|
103558
|
+
process.exit(2);
|
|
103559
|
+
}
|
|
103560
|
+
const pinned = (await focused()).filter((f) => f.pinned && f.root === entry.path && Date.now() - f.at < 3e4);
|
|
103561
|
+
if (pinned.length === 0) {
|
|
103562
|
+
console.error(`${entry.name} (${entry.path}) is a workspace, but no agent session was pinned to it.`);
|
|
103563
|
+
console.error("Run this from an agent's shell tool, with the errata daemon running: the daemon pins the session whose hook reports the command.");
|
|
103564
|
+
process.exit(1);
|
|
103565
|
+
}
|
|
103566
|
+
console.log(`focus: ${entry.name} (${entry.path}), pinned for session ${pinned.map((f) => f.sessionId.slice(0, 8)).join(", ")}.`);
|
|
103567
|
+
console.log("Captures and context now come from this workspace. `errata focus off` returns the session to its working directory.");
|
|
103568
|
+
}
|
|
103569
|
+
async function cmdRehome(args2) {
|
|
103570
|
+
const values = (flag) => args2.flatMap((a, i2) => a === flag && args2[i2 + 1] ? [args2[i2 + 1]] : []);
|
|
103571
|
+
const sessions = values("--session");
|
|
103572
|
+
const [fromRef] = values("--from");
|
|
103573
|
+
const [toRef] = values("--to");
|
|
103574
|
+
const apply = args2.includes("--apply");
|
|
103575
|
+
if (sessions.length === 0 || !fromRef || !toRef) {
|
|
103576
|
+
console.error("usage: errata rehome --session <id> [--session <id> \u2026] --from <workspace> --to <workspace> [--apply]");
|
|
103577
|
+
process.exit(2);
|
|
103578
|
+
}
|
|
103579
|
+
const entries = listWorkspaceEntries();
|
|
103580
|
+
const from = resolveWorkspaceRef(fromRef, entries, pickOwnerRoot);
|
|
103581
|
+
const to = resolveWorkspaceRef(toRef, entries, pickOwnerRoot);
|
|
103582
|
+
if (!from || !to || from.path === to.path) {
|
|
103583
|
+
console.error(`--from and --to must name two different workspaces (got ${from?.name ?? fromRef} \u2192 ${to?.name ?? toRef}).`);
|
|
103584
|
+
process.exit(2);
|
|
103585
|
+
}
|
|
103586
|
+
const { openGraphStore: openGraphStore2 } = await Promise.resolve().then(() => (init_src5(), src_exports2));
|
|
103587
|
+
const src = openGraphStore2({ path: workspacePaths(from.path).castalia });
|
|
103588
|
+
const dst = openGraphStore2({ path: workspacePaths(to.path).castalia });
|
|
103589
|
+
try {
|
|
103590
|
+
const r = rehomeSessionNodes(src, dst, sessions, { apply });
|
|
103591
|
+
const verb = apply ? "" : "would ";
|
|
103592
|
+
console.log(`${from.name} \u2192 ${to.name}${apply ? "" : " (dry run; --apply to write)"}`);
|
|
103593
|
+
console.log(` ${verb}move ${r.moved.length} node(s) (closed in ${from.name}), ${verb}copy ${r.copied.length} shared with other sessions`);
|
|
103594
|
+
console.log(` ${verb}carry ${r.edgesCarried} edge(s), ${verb}add ${r.vocabulary.length} vocabulary node(s) to ${to.name}; ${r.edgesDropped} edge(s) into ${from.name}'s code dropped`);
|
|
103595
|
+
if (r.cloudContributed.length > 0) {
|
|
103596
|
+
console.log(` ${r.cloudContributed.length} of them were already contributed to the cloud under ${from.name}'s project; those cloud copies are not moved.`);
|
|
103597
|
+
}
|
|
103598
|
+
} finally {
|
|
103599
|
+
src.close();
|
|
103600
|
+
dst.close();
|
|
103601
|
+
}
|
|
103602
|
+
}
|
|
103009
103603
|
async function cmdNotifications(state) {
|
|
103010
103604
|
const s = state.toLowerCase();
|
|
103011
103605
|
if (s !== "on" && s !== "off") {
|