@tekmidian/pai 0.36.2 → 0.38.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/dist/{auto-route-Byf8ENXj.mjs → auto-route-DM7GhJ8y.mjs} +2 -2
- package/dist/{auto-route-Byf8ENXj.mjs.map → auto-route-DM7GhJ8y.mjs.map} +1 -1
- package/dist/cli/index.mjs +5 -4
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.d.mts.map +1 -1
- package/dist/cli/program.mjs +5 -4
- package/dist/{clusters-wZgTCYCB.mjs → clusters-Do4tEGyc.mjs} +1 -1
- package/dist/{clusters-wZgTCYCB.mjs.map → clusters-Do4tEGyc.mjs.map} +1 -1
- package/dist/{context-handover-cache-PtNvj_8D.mjs → context-handover-cache-BpUojjsi.mjs} +2 -2
- package/dist/{context-handover-cache-PtNvj_8D.mjs.map → context-handover-cache-BpUojjsi.mjs.map} +1 -1
- package/dist/daemon/index.mjs +8 -8
- package/dist/{daemon-BZ93KRBo.mjs → daemon-CGg1VCbA.mjs} +25 -25
- package/dist/{daemon-BZ93KRBo.mjs.map → daemon-CGg1VCbA.mjs.map} +1 -1
- package/dist/daemon-DsGGiIJM.mjs +20 -0
- package/dist/daemon-mcp/index.mjs +549 -1
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{detector--Gg5JRN5.mjs → detector-CMap-9vw.mjs} +1 -1
- package/dist/{detector--Gg5JRN5.mjs.map → detector-CMap-9vw.mjs.map} +1 -1
- package/dist/detector-DtLExmHN.mjs +5 -0
- package/dist/factory-BXzqRYVZ.mjs +3 -0
- package/dist/{factory-Bsp7xOpO.mjs → factory-DD2T33C9.mjs} +5 -5
- package/dist/{factory-Bsp7xOpO.mjs.map → factory-DD2T33C9.mjs.map} +1 -1
- package/dist/hooks/context-compression-hook.mjs +164 -14
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/hooks/initialize-session.mjs +6 -0
- package/dist/hooks/initialize-session.mjs.map +3 -3
- package/dist/hooks/load-core-context.mjs +6 -0
- package/dist/hooks/load-core-context.mjs.map +3 -3
- package/dist/hooks/load-project-context.mjs +6 -0
- package/dist/hooks/load-project-context.mjs.map +3 -3
- package/dist/hooks/post-compact-inject.mjs +6 -0
- package/dist/hooks/post-compact-inject.mjs.map +3 -3
- package/dist/hooks/route-agents-to-worker.mjs +518 -0
- package/dist/hooks/route-agents-to-worker.mjs.map +7 -0
- package/dist/hooks/status-line.mjs +400 -0
- package/dist/hooks/status-line.mjs.map +7 -0
- package/dist/hooks/stop-hook.mjs +6 -0
- package/dist/hooks/stop-hook.mjs.map +3 -3
- package/dist/hooks/subagent-stop-hook.mjs +6 -0
- package/dist/hooks/subagent-stop-hook.mjs.map +3 -3
- package/dist/hooks/sync-todo-to-md.mjs +6 -0
- package/dist/hooks/sync-todo-to-md.mjs.map +3 -3
- package/dist/hooks/update-tab-on-action.mjs +6 -0
- package/dist/hooks/update-tab-on-action.mjs.map +3 -3
- package/dist/hooks/update-tab-titles.mjs +6 -0
- package/dist/hooks/update-tab-titles.mjs.map +3 -3
- package/dist/hooks/worker-proxy.mjs +858 -0
- package/dist/hooks/worker-proxy.mjs.map +7 -0
- package/dist/hooks/worker-status-line.mjs +545 -0
- package/dist/hooks/worker-status-line.mjs.map +7 -0
- package/dist/{indexer-backend-nQZuEx6N.mjs → indexer-backend-Cox9BCo-.mjs} +1 -1
- package/dist/{indexer-backend-nQZuEx6N.mjs.map → indexer-backend-Cox9BCo-.mjs.map} +1 -1
- package/dist/{kg-entity-DbOMPdF9.mjs → kg-entity-r8duqhi9.mjs} +1 -1
- package/dist/{kg-entity-DbOMPdF9.mjs.map → kg-entity-r8duqhi9.mjs.map} +1 -1
- package/dist/{latent-ideas-Bn6A5-5P.mjs → latent-ideas-BC1oINZ-.mjs} +2 -2
- package/dist/{latent-ideas-Bn6A5-5P.mjs.map → latent-ideas-BC1oINZ-.mjs.map} +1 -1
- package/dist/{link-boost-fYjUnxCN.mjs → link-boost-QFLrJwD6.mjs} +1 -1
- package/dist/{link-boost-fYjUnxCN.mjs.map → link-boost-QFLrJwD6.mjs.map} +1 -1
- package/dist/{main-resolver-BAbhKpeX.mjs → main-resolver-DlaLOFBA.mjs} +1 -1
- package/dist/{main-resolver-BAbhKpeX.mjs.map → main-resolver-DlaLOFBA.mjs.map} +1 -1
- package/dist/{main-resolver-Dxh444GO.mjs → main-resolver-IhZo4pI0.mjs} +1 -1
- package/dist/{neighborhood-DpaEM991.mjs → neighborhood-D9MJ1c8f.mjs} +1 -1
- package/dist/{neighborhood-DpaEM991.mjs.map → neighborhood-D9MJ1c8f.mjs.map} +1 -1
- package/dist/{note-context-DrcY4cWm.mjs → note-context-d1wT_-GA.mjs} +1 -1
- package/dist/{note-context-DrcY4cWm.mjs.map → note-context-d1wT_-GA.mjs.map} +1 -1
- package/dist/{postgres-BVme6qX0.mjs → postgres--BjPtLa0.mjs} +1 -1
- package/dist/{postgres-BVme6qX0.mjs.map → postgres--BjPtLa0.mjs.map} +1 -1
- package/dist/{program-C-fUghPv.mjs → program-Y0hAiVy8.mjs} +851 -101
- package/dist/program-Y0hAiVy8.mjs.map +1 -0
- package/dist/providers-DUshcB-d.mjs +4031 -0
- package/dist/providers-DUshcB-d.mjs.map +1 -0
- package/dist/{query-feedback-C1T6kS18.mjs → query-feedback-BUJxgw5B.mjs} +1 -1
- package/dist/{query-feedback-C1T6kS18.mjs.map → query-feedback-BUJxgw5B.mjs.map} +1 -1
- package/dist/query-feedback-DhyLOe5S.mjs +3 -0
- package/dist/router-1zi8jiNF.mjs +3 -0
- package/dist/{router-CsDm7HvK.mjs → router-DK_sLsUL.mjs} +1 -1
- package/dist/{router-CsDm7HvK.mjs.map → router-DK_sLsUL.mjs.map} +1 -1
- package/dist/skills/Worker/SKILL.md +63 -0
- package/dist/{sources-D8ZdNfvK.mjs → sources-Bi7--33T.mjs} +1 -1
- package/dist/{sources-D8ZdNfvK.mjs.map → sources-Bi7--33T.mjs.map} +1 -1
- package/dist/{sqlite-D1IaR8Am.mjs → sqlite-DtaL1glm.mjs} +1 -1
- package/dist/{sqlite-D1IaR8Am.mjs.map → sqlite-DtaL1glm.mjs.map} +1 -1
- package/dist/{state-qtmrBWCm.mjs → state-8Hm9E4tW.mjs} +1 -1
- package/dist/{state-WaXhLr6R.mjs → state-BY2L6-vX.mjs} +1 -1
- package/dist/{state-WaXhLr6R.mjs.map → state-BY2L6-vX.mjs.map} +1 -1
- package/dist/{themes-XPkj_bfP.mjs → themes-BN0a2duq.mjs} +1 -1
- package/dist/{themes-XPkj_bfP.mjs.map → themes-BN0a2duq.mjs.map} +1 -1
- package/dist/{tools-DEt6YPfc.mjs → tools-CGPqpU3A.mjs} +1 -1
- package/dist/{tools-ceiy7ANX.mjs → tools-y2bJpKom.mjs} +14 -14
- package/dist/{tools-ceiy7ANX.mjs.map → tools-y2bJpKom.mjs.map} +1 -1
- package/dist/{trace-DfyGmMG_.mjs → trace-h23JCcFD.mjs} +1 -1
- package/dist/{trace-DfyGmMG_.mjs.map → trace-h23JCcFD.mjs.map} +1 -1
- package/dist/{vault-indexer-CFvlPUMB.mjs → vault-indexer-DgsPjMgs.mjs} +1 -1
- package/dist/{vault-indexer-CFvlPUMB.mjs.map → vault-indexer-DgsPjMgs.mjs.map} +1 -1
- package/dist/{work-queue-worker-B8W8_3Rn.mjs → work-queue-worker-Dva_v_pI.mjs} +4 -4
- package/dist/{work-queue-worker-B8W8_3Rn.mjs.map → work-queue-worker-Dva_v_pI.mjs.map} +1 -1
- package/dist/{work-queue-worker-HN2Ufg-L.mjs → work-queue-worker-gsKd2LJa.mjs} +4 -4
- package/dist/{zettelkasten-CvjmMghT.mjs → zettelkasten-vo7psPdT.mjs} +3 -3
- package/dist/{zettelkasten-CvjmMghT.mjs.map → zettelkasten-vo7psPdT.mjs.map} +1 -1
- package/docs/commands/README.md +29 -0
- package/docs/commands/backup.md +1 -1
- package/docs/commands/clear-names.md +1 -1
- package/docs/commands/daemon.md +1 -1
- package/docs/commands/db.md +1 -1
- package/docs/commands/end.md +1 -1
- package/docs/commands/help.md +1 -1
- package/docs/commands/identity.md +1 -1
- package/docs/commands/kg.md +1 -1
- package/docs/commands/mcp.md +1 -1
- package/docs/commands/memory.md +1 -1
- package/docs/commands/notify.md +1 -1
- package/docs/commands/observation.md +1 -1
- package/docs/commands/obsidian.md +1 -1
- package/docs/commands/pause.md +1 -1
- package/docs/commands/project.md +1 -1
- package/docs/commands/projects.md +1 -1
- package/docs/commands/registry.md +1 -1
- package/docs/commands/restore.md +1 -1
- package/docs/commands/session.md +1 -1
- package/docs/commands/sessions.md +1 -1
- package/docs/commands/setup.md +1 -1
- package/docs/commands/shell-init.md +1 -1
- package/docs/commands/skill.md +1 -1
- package/docs/commands/task.md +1 -1
- package/docs/commands/topic.md +1 -1
- package/docs/commands/update.md +1 -1
- package/docs/commands/worker.md +398 -0
- package/docs/commands/zettel.md +1 -1
- package/docs/worker.md +434 -0
- package/package.json +1 -1
- package/scripts/build-hooks.mjs +46 -1
- package/src/hooks/pre-compact.sh +1 -0
- package/src/hooks/session-autosave.sh +1 -0
- package/src/hooks/session-stop.sh +1 -0
- package/src/hooks/ts/lib/context-fill.test.ts +104 -0
- package/src/hooks/ts/lib/context-fill.ts +73 -0
- package/src/hooks/ts/lib/handover-evidence.ts +227 -0
- package/src/hooks/ts/lib/worker-session.test.ts +24 -0
- package/src/hooks/ts/lib/worker-session.ts +23 -0
- package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +2 -0
- package/src/hooks/ts/post-tool-use/update-tab-on-action.ts +2 -0
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +17 -0
- package/src/hooks/ts/pre-tool-use/route-agents-to-worker.ts +128 -0
- package/src/hooks/ts/session-start/initialize-session.ts +2 -0
- package/src/hooks/ts/session-start/load-core-context.ts +2 -0
- package/src/hooks/ts/session-start/load-project-context.ts +2 -0
- package/src/hooks/ts/session-start/post-compact-inject.ts +2 -0
- package/src/hooks/ts/stop/stop-hook.ts +2 -0
- package/src/hooks/ts/subagent-stop/subagent-stop-hook.ts +2 -0
- package/src/hooks/ts/user-prompt/update-tab-titles.ts +2 -0
- package/statusline-command.sh +17 -0
- package/dist/daemon-DRdoA489.mjs +0 -20
- package/dist/detector-DGAk1iBR.mjs +0 -5
- package/dist/factory-CrokPMk2.mjs +0 -3
- package/dist/program-C-fUghPv.mjs.map +0 -1
- package/dist/query-feedback-BBMBp96K.mjs +0 -3
- package/dist/router-BMkOb62X.mjs +0 -3
|
@@ -365,6 +365,51 @@ function listProjectTranscripts(cwd: string, projectsDir: string): string[] {
|
|
|
365
365
|
return paths;
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
/** Placeholder model id the platform writes on synthetic assistant turns. */
|
|
369
|
+
const SYNTHETIC_MODEL = "<synthetic>";
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Which family of model wrote a transcript. "claude" is the platform's own;
|
|
373
|
+
* "foreign" is any other provider routed through the same CLI (a different
|
|
374
|
+
* context window, so its compactions say nothing about ours); "unknown" is
|
|
375
|
+
* no usable model field at all.
|
|
376
|
+
*/
|
|
377
|
+
export type TranscriptModelFamily = "claude" | "foreign" | "unknown";
|
|
378
|
+
|
|
379
|
+
export function modelFamily(model: string | null | undefined): TranscriptModelFamily {
|
|
380
|
+
if (typeof model !== "string" || model === "" || model === SYNTHETIC_MODEL) return "unknown";
|
|
381
|
+
return model.startsWith("claude-") ? "claude" : "foreign";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* The family of the LAST assistant model in a transcript — scanned from the
|
|
386
|
+
* end so a long transcript costs one read and a few lines of parsing.
|
|
387
|
+
* Unreadable or model-less transcripts are "unknown", never "foreign":
|
|
388
|
+
* the callers that skip work on "foreign" must not skip it on doubt.
|
|
389
|
+
*/
|
|
390
|
+
export function transcriptModelFamily(path: string): TranscriptModelFamily {
|
|
391
|
+
let raw: string;
|
|
392
|
+
try {
|
|
393
|
+
raw = readFileSync(path, "utf-8");
|
|
394
|
+
} catch {
|
|
395
|
+
return "unknown";
|
|
396
|
+
}
|
|
397
|
+
const lines = raw.split("\n");
|
|
398
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
399
|
+
const line = lines[i];
|
|
400
|
+
if (!line.includes('"model"')) continue;
|
|
401
|
+
try {
|
|
402
|
+
const entry = JSON.parse(line) as { type?: string; message?: { model?: unknown } };
|
|
403
|
+
if (entry.type !== "assistant") continue;
|
|
404
|
+
const model = entry.message?.model;
|
|
405
|
+
if (typeof model === "string" && model !== SYNTHETIC_MODEL) return modelFamily(model);
|
|
406
|
+
} catch {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return "unknown";
|
|
411
|
+
}
|
|
412
|
+
|
|
368
413
|
/**
|
|
369
414
|
* Every DISTINCT compact_boundary sample found across ALL of a project's
|
|
370
415
|
* transcripts (live + archived), newest first by the event's OWN timestamp
|
|
@@ -383,6 +428,17 @@ function readCompactBoundarySamples(cwd: string, projectsDir: string): CompactBo
|
|
|
383
428
|
continue;
|
|
384
429
|
}
|
|
385
430
|
|
|
431
|
+
// The model governing each sample: the most recent assistant
|
|
432
|
+
// `message.model` seen in this file before the compact_boundary. A
|
|
433
|
+
// transcript written by a non-Claude model (a headless worker on another
|
|
434
|
+
// provider, with a different context window) compacts at a different
|
|
435
|
+
// size and must not shape THIS project's trigger — two such workers
|
|
436
|
+
// compacting at ~151k pulled a real project's trigger from ~784k to
|
|
437
|
+
// ~151k. Samples with no model seen yet are kept: older transcripts
|
|
438
|
+
// may lack the field.
|
|
439
|
+
let lastModel: string | null = null;
|
|
440
|
+
let foreignDiscards = 0;
|
|
441
|
+
|
|
386
442
|
for (const line of raw.split("\n")) {
|
|
387
443
|
if (!line.trim()) continue;
|
|
388
444
|
let entry: {
|
|
@@ -391,23 +447,40 @@ function readCompactBoundarySamples(cwd: string, projectsDir: string): CompactBo
|
|
|
391
447
|
timestamp?: string;
|
|
392
448
|
uuid?: string;
|
|
393
449
|
compactMetadata?: { preTokens?: number };
|
|
450
|
+
message?: { model?: unknown };
|
|
394
451
|
};
|
|
395
452
|
try {
|
|
396
453
|
entry = JSON.parse(line);
|
|
397
454
|
} catch {
|
|
398
455
|
continue;
|
|
399
456
|
}
|
|
457
|
+
if (entry.type === "assistant") {
|
|
458
|
+
const model = entry.message?.model;
|
|
459
|
+
if (typeof model === "string" && model !== SYNTHETIC_MODEL) lastModel = model;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
400
462
|
if (entry.type !== "system" || entry.subtype !== "compact_boundary") continue;
|
|
401
463
|
const preTokens = entry.compactMetadata?.preTokens;
|
|
402
464
|
if (typeof preTokens !== "number" || !Number.isFinite(preTokens)) continue;
|
|
403
465
|
const timestampMs = entry.timestamp ? Date.parse(entry.timestamp) : NaN;
|
|
404
466
|
if (!Number.isFinite(timestampMs)) continue;
|
|
467
|
+
if (modelFamily(lastModel) === "foreign") {
|
|
468
|
+
foreignDiscards++;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
405
471
|
|
|
406
472
|
const key = entry.uuid ?? `${timestampMs}:${preTokens}`;
|
|
407
473
|
if (!byKey.has(key)) {
|
|
408
474
|
byKey.set(key, { preTokens, timestampMs, timestamp: entry.timestamp!, uuid: entry.uuid });
|
|
409
475
|
}
|
|
410
476
|
}
|
|
477
|
+
|
|
478
|
+
if (foreignDiscards > 0) {
|
|
479
|
+
console.error(
|
|
480
|
+
`[context-fill] ignored ${foreignDiscards} compaction sample(s) from a ` +
|
|
481
|
+
`non-Claude transcript (model=${lastModel}): ${path}`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
411
484
|
}
|
|
412
485
|
|
|
413
486
|
return [...byKey.values()].sort((a, b) => b.timestampMs - a.timestampMs);
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence binding for the model-written pre-compaction handover.
|
|
3
|
+
*
|
|
4
|
+
* The handover is prose written by a model about the session. Nothing else
|
|
5
|
+
* checks its claims, and the successor session cannot: by the time it reads
|
|
6
|
+
* the handover, the transcript it describes has been compacted away. This
|
|
7
|
+
* module does the one check that is cheap and deterministic — every
|
|
8
|
+
* checkable identifier in the handover (commit hashes, versions, paths,
|
|
9
|
+
* pids, timestamps, token counts) must appear verbatim in the transcript
|
|
10
|
+
* the handover was written from. Identifiers that do not are reported as
|
|
11
|
+
* unverified. Nothing is ever dropped or rewritten: the reader gets the
|
|
12
|
+
* handover unchanged plus one footer line saying how much of it is
|
|
13
|
+
* anchored in the record.
|
|
14
|
+
*
|
|
15
|
+
* Borrowed from the evidence-binding step of graph extractors that check an
|
|
16
|
+
* LLM's output against the chunk it was given. No model is called.
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { resolve } from "node:path";
|
|
20
|
+
import { contentToText } from "./transcript-text.js";
|
|
21
|
+
|
|
22
|
+
export interface EvidenceResult {
|
|
23
|
+
/** Distinct checkable identifiers found in the handover. */
|
|
24
|
+
total: number;
|
|
25
|
+
/** Identifiers present verbatim (after normalisation) in the transcript. */
|
|
26
|
+
found: string[];
|
|
27
|
+
/** Identifiers absent from the transcript — unverified, not wrong. */
|
|
28
|
+
missing: string[];
|
|
29
|
+
/** One line for the reader, never empty. */
|
|
30
|
+
footer: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Minimum identifier length worth checking; shorter strings match by accident. */
|
|
34
|
+
const MIN_TOKEN_LENGTH = 4;
|
|
35
|
+
/** How many unverified identifiers the footer names before "+N more". */
|
|
36
|
+
const FOOTER_NAMED_MISSING = 5;
|
|
37
|
+
/** Length of the handover's opening used to recognise its own echo in a tool result. */
|
|
38
|
+
const ECHO_HEAD_LENGTH = 80;
|
|
39
|
+
|
|
40
|
+
/** Markers of text that was injected INTO the session rather than produced by it. */
|
|
41
|
+
const INJECTED_MARKERS = [
|
|
42
|
+
"HANDOVER FROM THE PREVIOUS SESSION",
|
|
43
|
+
"MODEL-WRITTEN HANDOVER",
|
|
44
|
+
"SESSION STATE RECOVERED AFTER COMPACTION",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** Tools whose input would contain the handover itself when it targets the handover file. */
|
|
48
|
+
const HANDOVER_WRITERS = new Set(["Write", "Edit", "NotebookEdit"]);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Identifier classes, in priority order. Each pattern yields either its
|
|
52
|
+
* whole match or capture group 1 when present. Kept deliberately literal:
|
|
53
|
+
* an identifier is something a reader could grep for.
|
|
54
|
+
*/
|
|
55
|
+
const TOKEN_PATTERNS: RegExp[] = [
|
|
56
|
+
// git hashes: 7-40 hex chars with at least one letter (a plain number is not a hash)
|
|
57
|
+
/\b(?=[0-9a-f]*[a-f])[0-9a-f]{7,40}\b/g,
|
|
58
|
+
// package@version
|
|
59
|
+
/(?:@[\w.-]+\/)?[\w.-]+@\d+\.\d+\.\d+(?:-[\w.]+)?/g,
|
|
60
|
+
// semver
|
|
61
|
+
/\bv?\d+\.\d+\.\d+(?:-[\w.]+)?\b/g,
|
|
62
|
+
// absolute paths (two or more segments)
|
|
63
|
+
/(?:^|[\s(`'"])(\/[\w.-]+(?:\/[\w.-]+)+)/gm,
|
|
64
|
+
// relative paths with a slash and an extension
|
|
65
|
+
/(?:^|[\s(`'"])((?:~\/|\.\.?\/)?[\w.-]+(?:\/[\w.-]+)+\.[a-z0-9]{1,5})\b/gm,
|
|
66
|
+
// process ids
|
|
67
|
+
/\bpid\s*[:=#]?\s*(\d{3,7})\b/gi,
|
|
68
|
+
// ISO dates and timestamps
|
|
69
|
+
/\b\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?Z?)?\b/g,
|
|
70
|
+
// numbers with units — digits only are kept
|
|
71
|
+
/\b(\d[\d,]*)\s*(?:tokens?|ms|MB|GB)\b/g,
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
/** Strip thousands separators so "784,000" and "784000" compare equal. */
|
|
75
|
+
function normalise(text: string): string {
|
|
76
|
+
return text.replace(/(\d),(?=\d{3}\b)/g, "$1");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stripSystemReminders(text: string): string {
|
|
80
|
+
return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Every distinct checkable identifier in a handover, in order of first appearance. */
|
|
84
|
+
export function extractCheckableTokens(markdown: string): string[] {
|
|
85
|
+
const seen = new Set<string>();
|
|
86
|
+
const out: string[] = [];
|
|
87
|
+
for (const pattern of TOKEN_PATTERNS) {
|
|
88
|
+
pattern.lastIndex = 0;
|
|
89
|
+
for (const match of markdown.matchAll(pattern)) {
|
|
90
|
+
const raw = (match[1] ?? match[0]).trim();
|
|
91
|
+
const token = normalise(raw);
|
|
92
|
+
if (token.length < MIN_TOKEN_LENGTH || seen.has(token)) continue;
|
|
93
|
+
seen.add(token);
|
|
94
|
+
out.push(token);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface TranscriptEntry {
|
|
101
|
+
type?: string;
|
|
102
|
+
message?: { content?: unknown };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface ContentPart {
|
|
106
|
+
type?: string;
|
|
107
|
+
text?: string;
|
|
108
|
+
name?: string;
|
|
109
|
+
input?: { file_path?: unknown } & Record<string, unknown>;
|
|
110
|
+
content?: unknown;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Everything the session itself said, did, or was shown — minus the two
|
|
115
|
+
* places where the handover could verify against itself: the tool call
|
|
116
|
+
* that wrote the handover file, and injected blocks carried over from an
|
|
117
|
+
* earlier compaction.
|
|
118
|
+
*/
|
|
119
|
+
function buildHaystack(
|
|
120
|
+
transcriptPaths: string[],
|
|
121
|
+
handoverPath: string | null,
|
|
122
|
+
handoverHead: string
|
|
123
|
+
): string {
|
|
124
|
+
const parts: string[] = [];
|
|
125
|
+
const resolvedHandover = handoverPath ? resolve(handoverPath) : null;
|
|
126
|
+
|
|
127
|
+
const pushUserText = (text: string): void => {
|
|
128
|
+
if (INJECTED_MARKERS.some((m) => text.includes(m))) return;
|
|
129
|
+
parts.push(stripSystemReminders(text));
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
for (const path of transcriptPaths) {
|
|
133
|
+
let raw: string;
|
|
134
|
+
try {
|
|
135
|
+
raw = readFileSync(path, "utf-8");
|
|
136
|
+
} catch {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
for (const line of raw.split("\n")) {
|
|
140
|
+
if (!line.trim()) continue;
|
|
141
|
+
let entry: TranscriptEntry;
|
|
142
|
+
try {
|
|
143
|
+
entry = JSON.parse(line) as TranscriptEntry;
|
|
144
|
+
} catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const content = entry.message?.content;
|
|
148
|
+
if (content === undefined) continue;
|
|
149
|
+
|
|
150
|
+
if (entry.type === "user") {
|
|
151
|
+
if (typeof content === "string") {
|
|
152
|
+
pushUserText(content);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!Array.isArray(content)) continue;
|
|
156
|
+
for (const part of content as ContentPart[]) {
|
|
157
|
+
if (part?.type === "text" && typeof part.text === "string") pushUserText(part.text);
|
|
158
|
+
else if (part?.type === "tool_result") {
|
|
159
|
+
const text = contentToText(part.content);
|
|
160
|
+
if (handoverHead && text.startsWith(handoverHead)) continue;
|
|
161
|
+
parts.push(text);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} else if (entry.type === "assistant" && Array.isArray(content)) {
|
|
165
|
+
for (const part of content as ContentPart[]) {
|
|
166
|
+
if (part?.type === "text" && typeof part.text === "string") parts.push(part.text);
|
|
167
|
+
else if (part?.type === "tool_use") {
|
|
168
|
+
const target = part.input?.file_path;
|
|
169
|
+
if (
|
|
170
|
+
resolvedHandover &&
|
|
171
|
+
HANDOVER_WRITERS.has(part.name ?? "") &&
|
|
172
|
+
typeof target === "string" &&
|
|
173
|
+
resolve(target) === resolvedHandover
|
|
174
|
+
) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
parts.push(JSON.stringify(part.input ?? {}));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return normalise(parts.join("\n"));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function footerFor(found: string[], missing: string[]): string {
|
|
187
|
+
const total = found.length + missing.length;
|
|
188
|
+
if (total === 0) return "Evidence: no checkable identifiers";
|
|
189
|
+
let footer = `Evidence: ${found.length}/${total} identifiers found in the transcript`;
|
|
190
|
+
if (missing.length > 0) {
|
|
191
|
+
const named = missing.slice(0, FOOTER_NAMED_MISSING).join(", ");
|
|
192
|
+
const more = missing.length - FOOTER_NAMED_MISSING;
|
|
193
|
+
footer += `; unverified: ${named}${more > 0 ? ` +${more} more` : ""}`;
|
|
194
|
+
}
|
|
195
|
+
return footer;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check a handover's identifiers against the transcript(s) it was written
|
|
200
|
+
* from. Never throws: any failure degrades to a footer that says the check
|
|
201
|
+
* was unavailable, so the handover itself is always delivered.
|
|
202
|
+
*
|
|
203
|
+
* @param handoverMarkdown the handover text, returned to the reader unchanged
|
|
204
|
+
* @param handoverPath where the handover was written, if it was written
|
|
205
|
+
* to a file — its own Write/Edit is excluded as evidence
|
|
206
|
+
* @param transcriptPaths the session's transcript file(s)
|
|
207
|
+
*/
|
|
208
|
+
export function bindHandoverEvidence(
|
|
209
|
+
handoverMarkdown: string,
|
|
210
|
+
handoverPath: string | null,
|
|
211
|
+
transcriptPaths: string[]
|
|
212
|
+
): EvidenceResult {
|
|
213
|
+
try {
|
|
214
|
+
const tokens = extractCheckableTokens(handoverMarkdown);
|
|
215
|
+
if (tokens.length === 0) {
|
|
216
|
+
return { total: 0, found: [], missing: [], footer: footerFor([], []) };
|
|
217
|
+
}
|
|
218
|
+
const head = handoverMarkdown.trim().slice(0, ECHO_HEAD_LENGTH);
|
|
219
|
+
const haystack = buildHaystack(transcriptPaths, handoverPath, head);
|
|
220
|
+
const found: string[] = [];
|
|
221
|
+
const missing: string[] = [];
|
|
222
|
+
for (const token of tokens) (haystack.includes(token) ? found : missing).push(token);
|
|
223
|
+
return { total: tokens.length, found, missing, footer: footerFor(found, missing) };
|
|
224
|
+
} catch {
|
|
225
|
+
return { total: 0, found: [], missing: [], footer: "Evidence: check unavailable" };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { isWorkerSession } from "./worker-session.js";
|
|
3
|
+
|
|
4
|
+
describe("isWorkerSession", () => {
|
|
5
|
+
it("is true only for the exact launcher mark PAI_WORKER=1", () => {
|
|
6
|
+
expect(isWorkerSession({ PAI_WORKER: "1" })).toBe(true);
|
|
7
|
+
expect(isWorkerSession({ PAI_WORKER: "true" })).toBe(false);
|
|
8
|
+
expect(isWorkerSession({ PAI_WORKER: "" })).toBe(false);
|
|
9
|
+
expect(isWorkerSession({})).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("reads process.env by default", () => {
|
|
13
|
+
const prev = process.env.PAI_WORKER;
|
|
14
|
+
try {
|
|
15
|
+
process.env.PAI_WORKER = "1";
|
|
16
|
+
expect(isWorkerSession()).toBe(true);
|
|
17
|
+
delete process.env.PAI_WORKER;
|
|
18
|
+
expect(isWorkerSession()).toBe(false);
|
|
19
|
+
} finally {
|
|
20
|
+
if (prev === undefined) delete process.env.PAI_WORKER;
|
|
21
|
+
else process.env.PAI_WORKER = prev;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-session detection.
|
|
3
|
+
*
|
|
4
|
+
* A disposable headless worker (a `claude -p` run started by an orchestrating
|
|
5
|
+
* session, possibly against a different model provider with a different
|
|
6
|
+
* context window) shares the project directory and the hook configuration
|
|
7
|
+
* with the real session that spawned it. Left alone, the hooks treat it as a
|
|
8
|
+
* session in its own right: they inject project context into it, create and
|
|
9
|
+
* rename a numbered session note for it, autosave it, and enqueue a
|
|
10
|
+
* model-written handover for it. Its compactions also land in the project's
|
|
11
|
+
* transcript folder, where they are indistinguishable from a real session's
|
|
12
|
+
* and drag the measured compaction trigger down (observed 2026-09-17: two
|
|
13
|
+
* workers compacting at ~151k tokens pulled a project's trigger from ~784k
|
|
14
|
+
* to ~151k, and the real session's handover fired at ~50k tokens).
|
|
15
|
+
*
|
|
16
|
+
* The launcher marks such sessions with `PAI_WORKER=1`. Every hook that does
|
|
17
|
+
* per-session bookkeeping returns immediately when this predicate is true.
|
|
18
|
+
* Deliberately NOT guarded: the security validator (a worker's shell
|
|
19
|
+
* commands must still be checked) and observability capture.
|
|
20
|
+
*/
|
|
21
|
+
export function isWorkerSession(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
22
|
+
return env.PAI_WORKER === "1";
|
|
23
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* IMPORTANT: This hook PRESERVES user content. It only updates "Current Session".
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { isWorkerSession } from "../lib/worker-session.js";
|
|
13
14
|
import { writeFileSync, existsSync, readFileSync, mkdirSync } from 'fs';
|
|
14
15
|
import { join } from 'path';
|
|
15
16
|
import {
|
|
@@ -131,6 +132,7 @@ ${formatSessionTodos(todos)}`;
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
async function main() {
|
|
135
|
+
if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping
|
|
134
136
|
try {
|
|
135
137
|
const chunks: Buffer[] = [];
|
|
136
138
|
for await (const chunk of process.stdin) {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* PAI-safe: No sensitive data, just tool descriptions
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { isWorkerSession } from "../lib/worker-session.js";
|
|
10
11
|
import { execSync } from 'child_process';
|
|
11
12
|
|
|
12
13
|
// Action tools that warrant a tab title update
|
|
@@ -122,6 +123,7 @@ function generateTitle(toolName: string, input: HookInput['tool_input']): string
|
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
async function main() {
|
|
126
|
+
if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping
|
|
125
127
|
try {
|
|
126
128
|
const input = await readStdinWithTimeout();
|
|
127
129
|
const data: HookInput = JSON.parse(input);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* and work items from earlier in the session.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { isWorkerSession } from "../lib/worker-session.js";
|
|
16
17
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
17
18
|
import { basename, dirname, join } from 'path';
|
|
18
19
|
import { tmpdir } from 'os';
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
import { getContextFill, formatContextFill } from '../lib/context-fill.js';
|
|
35
36
|
import { contentToText, isNoiseFilePath, preferCwdFiles } from '../lib/transcript-text.js';
|
|
36
37
|
import { readContextHandoverCache } from '../lib/context-handover-cache.js';
|
|
38
|
+
import { bindHandoverEvidence } from '../lib/handover-evidence.js';
|
|
37
39
|
|
|
38
40
|
interface HookInput {
|
|
39
41
|
session_id: string;
|
|
@@ -515,6 +517,7 @@ function enqueueSessionSummary(payload: {
|
|
|
515
517
|
// ---------------------------------------------------------------------------
|
|
516
518
|
|
|
517
519
|
async function main() {
|
|
520
|
+
if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping
|
|
518
521
|
// Skip probe/health-check sessions (e.g. CodexBar ClaudeProbe)
|
|
519
522
|
if (isProbeSession()) {
|
|
520
523
|
process.exit(0);
|
|
@@ -714,11 +717,13 @@ async function main() {
|
|
|
714
717
|
// -------------------------------------------------------------------
|
|
715
718
|
let sourceLabel: string;
|
|
716
719
|
let handoverBlock: string;
|
|
720
|
+
let handoverSummary: string | null = null;
|
|
717
721
|
try {
|
|
718
722
|
const cachedHandover = readContextHandoverCache(hookInput.session_id);
|
|
719
723
|
const handoverIsFresh =
|
|
720
724
|
cachedHandover !== null &&
|
|
721
725
|
(!previousCompactionAt || new Date(cachedHandover.generatedAt) > new Date(previousCompactionAt));
|
|
726
|
+
handoverSummary = handoverIsFresh ? cachedHandover!.summary : null;
|
|
722
727
|
|
|
723
728
|
sourceLabel = handoverIsFresh
|
|
724
729
|
? `model-written handover (${cachedHandover!.model}, generated ${cachedHandover!.generatedAt}, ` +
|
|
@@ -739,6 +744,18 @@ async function main() {
|
|
|
739
744
|
handoverBlock = '';
|
|
740
745
|
}
|
|
741
746
|
|
|
747
|
+
// Evidence binding: which of the handover's identifiers the transcript
|
|
748
|
+
// actually contains. One footer line, content untouched; its own
|
|
749
|
+
// failure changes nothing else about the injection.
|
|
750
|
+
if (handoverSummary !== null && typeof hookInput.transcript_path === 'string') {
|
|
751
|
+
try {
|
|
752
|
+
const evidence = bindHandoverEvidence(handoverSummary, null, [hookInput.transcript_path]);
|
|
753
|
+
handoverBlock += `\n${evidence.footer}`;
|
|
754
|
+
} catch (err) {
|
|
755
|
+
console.error(`Evidence check skipped: ${err}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
742
759
|
const injection = [
|
|
743
760
|
'<system-reminder>',
|
|
744
761
|
`SESSION STATE RECOVERED AFTER COMPACTION (${compactType}, ~${tokenDisplay} tokens)`,
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* route-agents-to-worker.ts — PreToolUse hook on the Agent tool.
|
|
5
|
+
*
|
|
6
|
+
* Denies every in-process subagent when worker providers are configured, so no
|
|
7
|
+
* worker ever runs on Anthropic. The deny reason tells the orchestrator how to
|
|
8
|
+
* delegate through `pai worker run` instead — same idea as the old
|
|
9
|
+
* route-agents-to-glm.sh, but provider-neutral and ledgered in the workers
|
|
10
|
+
* log dir.
|
|
11
|
+
*
|
|
12
|
+
* Allows (and does not touch) when:
|
|
13
|
+
* - the tool is not the Agent tool (this hook is Agent-matched, but stay safe)
|
|
14
|
+
* - ALLOW_ANTHROPIC_AGENTS=1 is in the environment (one-session bypass)
|
|
15
|
+
* - workers are off or no provider is configured
|
|
16
|
+
*
|
|
17
|
+
* Every decision is appended to the routing ledger read by `pai worker log`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readWorkersSection } from "../../../workers/config.js";
|
|
21
|
+
import { workersLogDir, ledgerPath } from "../../../workers/paths.js";
|
|
22
|
+
import { appendLedger } from "../../../workers/ledger.js";
|
|
23
|
+
|
|
24
|
+
interface HookInput {
|
|
25
|
+
session_id?: string;
|
|
26
|
+
cwd?: string;
|
|
27
|
+
tool_name?: string;
|
|
28
|
+
tool_input?: { description?: string; prompt?: string; subagent_type?: string } | string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ALLOW = JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } });
|
|
32
|
+
|
|
33
|
+
function deny(reason: string): string {
|
|
34
|
+
return JSON.stringify({
|
|
35
|
+
hookSpecificOutput: {
|
|
36
|
+
hookEventName: "PreToolUse",
|
|
37
|
+
permissionDecision: "deny",
|
|
38
|
+
permissionDecisionReason: reason,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function shortLabel(input: HookInput): string {
|
|
44
|
+
const desc =
|
|
45
|
+
typeof input.tool_input === "object" && input.tool_input !== null
|
|
46
|
+
? input.tool_input.description ?? ""
|
|
47
|
+
: "";
|
|
48
|
+
return desc.replace(/\s+/g, " ").trim().slice(0, 60);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function main(): Promise<void> {
|
|
52
|
+
let text = "";
|
|
53
|
+
try {
|
|
54
|
+
for await (const chunk of process.stdin) text += chunk;
|
|
55
|
+
} catch {
|
|
56
|
+
process.stdout.write(ALLOW);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!text.trim()) {
|
|
60
|
+
process.stdout.write(ALLOW);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let input: HookInput;
|
|
65
|
+
try {
|
|
66
|
+
input = JSON.parse(text) as HookInput;
|
|
67
|
+
} catch {
|
|
68
|
+
process.stdout.write(ALLOW);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (input.tool_name && input.tool_name !== "Agent") {
|
|
73
|
+
process.stdout.write(ALLOW);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const label = shortLabel(input);
|
|
78
|
+
const cwd = input.cwd ?? process.cwd();
|
|
79
|
+
|
|
80
|
+
const ledger = (event: string): void => {
|
|
81
|
+
try {
|
|
82
|
+
appendLedger(ledgerPath(workersLogDir(readWorkersSection().workers)), event, {
|
|
83
|
+
cwd,
|
|
84
|
+
...(label ? { label } : {}),
|
|
85
|
+
});
|
|
86
|
+
} catch {
|
|
87
|
+
// no config / no log dir yet — the decision below still stands
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (process.env.ALLOW_ANTHROPIC_AGENTS === "1") {
|
|
92
|
+
ledger("ALLOWED-ANTHROPIC-AGENT");
|
|
93
|
+
process.stdout.write(ALLOW);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let workers: ReturnType<typeof readWorkersSection>["workers"];
|
|
98
|
+
try {
|
|
99
|
+
workers = readWorkersSection().workers;
|
|
100
|
+
} catch {
|
|
101
|
+
// broken workers config must not break the session
|
|
102
|
+
process.stdout.write(ALLOW);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!workers.enabled || Object.keys(workers.providers).length === 0) {
|
|
106
|
+
process.stdout.write(ALLOW);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
ledger("DENIED-ANTHROPIC-AGENT");
|
|
111
|
+
process.stdout.write(
|
|
112
|
+
deny(
|
|
113
|
+
"Agent tool is disabled: subagents run on the configured worker provider, not Anthropic. " +
|
|
114
|
+
"Delegate with Bash instead, in the background:\n\n" +
|
|
115
|
+
`pai worker run --label "${label || "task"}" --role research -p '<full, self-contained task spec>' ` +
|
|
116
|
+
"--allowedTools 'Read,Edit,Write,Bash,Grep,Glob' --output-format json\n\n" +
|
|
117
|
+
"- Run it with run_in_background: true and always with a timeout.\n" +
|
|
118
|
+
"- Use --role spotcheck (or implement) as the task demands.\n" +
|
|
119
|
+
"- Web research: add WebSearch,WebFetch to --allowedTools.\n" +
|
|
120
|
+
"- The answer is in the `result` field of the JSON it prints. Review the diff yourself.\n" +
|
|
121
|
+
"- pai worker ps lists running workers; pai worker follow <id> shows one live.\n" +
|
|
122
|
+
"Keep only orchestration, review and synthesis in this session. " +
|
|
123
|
+
"To run subagents on Anthropic for one session: start it with ALLOW_ANTHROPIC_AGENTS=1."
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
main().catch(() => process.stdout.write(ALLOW));
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* initialize-session.ts, load-core-context.ts, load-project-context.ts
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import { isWorkerSession } from "../lib/worker-session.js";
|
|
23
24
|
import { existsSync, statSync, readFileSync, writeFileSync } from 'fs';
|
|
24
25
|
import { join } from 'path';
|
|
25
26
|
import { tmpdir } from 'os';
|
|
@@ -99,6 +100,7 @@ async function testStopHook() {
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
async function main() {
|
|
103
|
+
if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping
|
|
102
104
|
try {
|
|
103
105
|
// Check if this is a subagent session - if so, exit silently
|
|
104
106
|
const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || '';
|
|
@@ -25,12 +25,14 @@
|
|
|
25
25
|
* - Gives your AI immediate access to your complete personal context
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
import { isWorkerSession } from "../lib/worker-session.js";
|
|
28
29
|
import { readFileSync, existsSync } from 'fs';
|
|
29
30
|
import { join } from 'path';
|
|
30
31
|
import { PAI_DIR, SKILLS_DIR } from '../lib/pai-paths';
|
|
31
32
|
import { isProbeSession } from '../lib/project-utils';
|
|
32
33
|
|
|
33
34
|
async function main() {
|
|
35
|
+
if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping
|
|
34
36
|
try {
|
|
35
37
|
// Check if this is a subagent session - if so, exit silently
|
|
36
38
|
const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || '';
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* - Sending ntfy.sh notifications
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { isWorkerSession } from "../lib/worker-session.js";
|
|
19
20
|
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
20
21
|
import { join, basename, dirname, resolve } from 'path';
|
|
21
22
|
import { homedir } from 'os';
|
|
@@ -141,6 +142,7 @@ interface HookInput {
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
async function main() {
|
|
145
|
+
if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping
|
|
144
146
|
console.error('\nload-project-context.ts starting...');
|
|
145
147
|
|
|
146
148
|
// Skip probe/health-check sessions (e.g. CodexBar ClaudeProbe)
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* SessionStart(compact) → THIS HOOK reads that file → stdout → context
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { isWorkerSession } from "../lib/worker-session.js";
|
|
19
20
|
import { readFileSync, existsSync, unlinkSync } from 'fs';
|
|
20
21
|
import { join } from 'path';
|
|
21
22
|
import { tmpdir } from 'os';
|
|
@@ -30,6 +31,7 @@ interface HookInput {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
async function main() {
|
|
34
|
+
if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping
|
|
33
35
|
let hookInput: HookInput | null = null;
|
|
34
36
|
|
|
35
37
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { isWorkerSession } from "../lib/worker-session.js";
|
|
3
4
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
|
|
4
5
|
import { join, basename, dirname } from 'path';
|
|
5
6
|
import { connect } from 'net';
|
|
@@ -704,6 +705,7 @@ function debug(msg: string): void {
|
|
|
704
705
|
}
|
|
705
706
|
|
|
706
707
|
async function main() {
|
|
708
|
+
if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping
|
|
707
709
|
if (isProbeSession()) {
|
|
708
710
|
process.exit(0);
|
|
709
711
|
}
|