@gcunharodrigues/wrxn 0.11.0 → 0.12.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/lib/install.cjs +3 -0
- package/manifest.json +15 -5
- package/migrations/007-auto-memory-transition.cjs +136 -0
- package/package.json +1 -1
- package/payload/.claude/hooks/memory-synth-spawn.cjs +101 -0
- package/payload/.claude/hooks/session-start.cjs +86 -8
- package/payload/.claude/hooks/synapse-engine.cjs +2 -3
- package/payload/.claude/settings.json +7 -0
- package/payload/.claude/skills/dream/SKILL.md +2 -31
- package/payload/.claude/skills/synapse/SKILL.md +5 -5
- package/payload/.claude/skills/synapse/references/brackets.md +1 -2
- package/payload/.claude/skills/synapse/references/layers.md +1 -2
- package/payload/.wrxn/dream.cjs +112 -66
- package/payload/.wrxn/memory-synth.cjs +761 -0
- package/payload/.wrxn/memory.config.json +12 -0
- package/payload/.wrxn/wiki.cjs +7 -19
- package/payload/.claude/skills/handoff/SKILL.md +0 -22
package/lib/install.cjs
CHANGED
|
@@ -86,6 +86,9 @@ function init(opts) {
|
|
|
86
86
|
ensureGitignoreLine(target, '.recon-wrxn/');
|
|
87
87
|
// the recall-surface hook writes per-install access-recency state here (harvest-08) — runtime, not committed.
|
|
88
88
|
ensureGitignoreLine(target, '.wrxn/reinforce.json');
|
|
89
|
+
// the memory-synth gemini fallback reads GEMINI_API_KEY from `.env` — ignore it so the secret is
|
|
90
|
+
// never committed (the synth's doc-comment calls it the install's gitignored `.env`).
|
|
91
|
+
ensureGitignoreLine(target, '.env');
|
|
89
92
|
|
|
90
93
|
writeReceipt(target, { version, profile, laid, skipped, merged, brownfield });
|
|
91
94
|
|
package/manifest.json
CHANGED
|
@@ -73,6 +73,11 @@
|
|
|
73
73
|
"class": "managed",
|
|
74
74
|
"profile": "project"
|
|
75
75
|
},
|
|
76
|
+
{
|
|
77
|
+
"path": ".claude/hooks/memory-synth-spawn.cjs",
|
|
78
|
+
"class": "managed",
|
|
79
|
+
"profile": "project"
|
|
80
|
+
},
|
|
76
81
|
{
|
|
77
82
|
"path": ".claude/hooks/recall-surface.cjs",
|
|
78
83
|
"class": "managed",
|
|
@@ -143,11 +148,6 @@
|
|
|
143
148
|
"class": "managed",
|
|
144
149
|
"profile": "project"
|
|
145
150
|
},
|
|
146
|
-
{
|
|
147
|
-
"path": ".claude/skills/handoff/SKILL.md",
|
|
148
|
-
"class": "managed",
|
|
149
|
-
"profile": "project"
|
|
150
|
-
},
|
|
151
151
|
{
|
|
152
152
|
"path": ".claude/skills/harvest/SKILL.md",
|
|
153
153
|
"class": "managed",
|
|
@@ -488,6 +488,16 @@
|
|
|
488
488
|
"class": "state",
|
|
489
489
|
"profile": "project"
|
|
490
490
|
},
|
|
491
|
+
{
|
|
492
|
+
"path": ".wrxn/memory-synth.cjs",
|
|
493
|
+
"class": "managed",
|
|
494
|
+
"profile": "project"
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
"path": ".wrxn/memory.config.json",
|
|
498
|
+
"class": "seeded",
|
|
499
|
+
"profile": "project"
|
|
500
|
+
},
|
|
491
501
|
{
|
|
492
502
|
"path": ".wrxn/sync.cjs",
|
|
493
503
|
"class": "managed",
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 007 — transition existing installs onto auto-memory (auto-memory-05).
|
|
8
|
+
*
|
|
9
|
+
* The auto-memory release makes memory automatic: a SessionEnd hook (memory-synth-spawn.cjs) spawns a
|
|
10
|
+
* background synth that writes the continuity baton and consolidates dream pages — so the manual
|
|
11
|
+
* `handoff` skill is removed (the synth is the sole baton writer) and the stale `_slots/current-focus`
|
|
12
|
+
* slot + its `set-focus` op are dropped. The new payload stops shipping the handoff skill and now wires
|
|
13
|
+
* SessionEnd + ships a seeded `memory.config.json`. But `wrxn update` overwrites managed files in place —
|
|
14
|
+
* it never PRUNES a removed one — and a seeded file already present is preserved, so a pre-0.12.0 install
|
|
15
|
+
* still carries the old `handoff` skill files and the stale focus slot, and may lack the new wiring/seed
|
|
16
|
+
* (e.g. a hand-edited settings.json the managed overwrite left alone, or an install that never had a
|
|
17
|
+
* config). up() transitions it.
|
|
18
|
+
*
|
|
19
|
+
* Steps: (1) remove the install's `handoff` skill dir; (2) wire SessionEnd → memory-synth-spawn.cjs into
|
|
20
|
+
* the install settings.json IDEMPOTENTLY (add only if absent — if update's managed overwrite already laid
|
|
21
|
+
* it, this is a safe no-op); a corrupt settings.json is left untouched; (3) seed `memory.config.json` if
|
|
22
|
+
* absent (the slice-02 default shape); (4) remove the stale `_slots/current-focus.md` focus slot;
|
|
23
|
+
* (5) backfill the install `.gitignore` for the `.env` secret and the continuity runtime temps the synth/
|
|
24
|
+
* dream now write (`.wrxn/continuity/.pending`, `.pending-handoff`, the baton `.tmp`, `.dream.*.tmp`) —
|
|
25
|
+
* slice-02 added `.env` for NEW installs; this closes the gap for OLD ones (slice-04 reviewer F1).
|
|
26
|
+
*
|
|
27
|
+
* Defensive like 004: every step is existence-guarded and best-effort (force-rm ignores a missing file),
|
|
28
|
+
* a missing/clean target is a no-op, a corrupt settings.json is left untouched (never clobber a hand-
|
|
29
|
+
* edited file — the other steps still run), and the gitignore backfill adds each line at most once.
|
|
30
|
+
* Idempotent (a second run finds nothing to do) and never throws on an already-clean install. `version`
|
|
31
|
+
* 0.12.0 = the auto-memory release that carries the transition (the same release whose payload stops
|
|
32
|
+
* shipping the handoff skill and starts wiring the SessionEnd synth).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
// The .gitignore lines auto-memory needs an install to carry: the `.env` secret (slice-02 added it for
|
|
36
|
+
// NEW installs — backfilled here for OLD ones) + the continuity runtime markers/temps the synth and dream
|
|
37
|
+
// write and clean in a finally, which a SIGKILL could leave behind UNTRACKED (slice-04 reviewer F1). The
|
|
38
|
+
// tracked baton `latest.md` is deliberately NOT ignored — only its dot-prefixed `.tmp` is.
|
|
39
|
+
const GITIGNORE_LINES = [
|
|
40
|
+
'.env',
|
|
41
|
+
'.wrxn/continuity/.pending*', // the .pending + .pending-handoff synth markers
|
|
42
|
+
'.wrxn/continuity/.dream.*.tmp', // dream's per-call blob/batch/stage/approved temps
|
|
43
|
+
'.wrxn/continuity/.latest.md.*.tmp', // the atomic baton-write temp (rename-over target)
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/** Append `line` to `<target>/.gitignore` (create if absent) exactly once — mirrors install.cjs. */
|
|
47
|
+
function ensureGitignoreLine(target, line) {
|
|
48
|
+
const giPath = path.join(target, '.gitignore');
|
|
49
|
+
let body = '';
|
|
50
|
+
try {
|
|
51
|
+
body = fs.readFileSync(giPath, 'utf8');
|
|
52
|
+
} catch {
|
|
53
|
+
body = '';
|
|
54
|
+
}
|
|
55
|
+
if (body.split('\n').some((l) => l.trim() === line)) return; // already ignored
|
|
56
|
+
const prefix = body.length && !body.endsWith('\n') ? '\n' : '';
|
|
57
|
+
fs.writeFileSync(giPath, body + prefix + line + '\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The SessionEnd spawn hook the auto-memory payload wires (must match payload/.claude/settings.json).
|
|
61
|
+
const SPAWN_HOOK_BASENAME = 'memory-synth-spawn.cjs';
|
|
62
|
+
const SPAWN_HOOK_COMMAND = 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/memory-synth-spawn.cjs"';
|
|
63
|
+
|
|
64
|
+
// Idempotently wire SessionEnd → the spawn hook into a parsed settings config. Adds the hook ONLY if no
|
|
65
|
+
// existing hook command across any event already references it — so a config the managed overwrite already
|
|
66
|
+
// laid (or a prior run) is a safe no-op. Returns true iff the config changed. Preserves every other event.
|
|
67
|
+
function wireSessionEndSpawn(cfg) {
|
|
68
|
+
if (!cfg || typeof cfg !== 'object') return false;
|
|
69
|
+
cfg.hooks = cfg.hooks && typeof cfg.hooks === 'object' ? cfg.hooks : {};
|
|
70
|
+
// already wired anywhere? (mirror unwireHook's whole-config scan) → no-op.
|
|
71
|
+
for (const groups of Object.values(cfg.hooks)) {
|
|
72
|
+
if (!Array.isArray(groups)) continue;
|
|
73
|
+
for (const group of groups) {
|
|
74
|
+
if (!group || !Array.isArray(group.hooks)) continue;
|
|
75
|
+
if (group.hooks.some((h) => h && typeof h.command === 'string' && h.command.includes(SPAWN_HOOK_BASENAME))) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const event = Array.isArray(cfg.hooks.SessionEnd) ? cfg.hooks.SessionEnd : [];
|
|
81
|
+
event.push({ hooks: [{ type: 'command', command: SPAWN_HOOK_COMMAND }] });
|
|
82
|
+
cfg.hooks.SessionEnd = event;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
id: '007',
|
|
88
|
+
version: '0.12.0',
|
|
89
|
+
up(ctx) {
|
|
90
|
+
const target = ctx.target;
|
|
91
|
+
|
|
92
|
+
// 1. remove the retired `handoff` skill dir (the synth is the sole baton writer now). recursive +
|
|
93
|
+
// force = an absent dir is a no-op.
|
|
94
|
+
fs.rmSync(path.join(target, '.claude', 'skills', 'handoff'), { recursive: true, force: true });
|
|
95
|
+
|
|
96
|
+
// 2. wire SessionEnd → memory-synth-spawn.cjs into the install settings.json, idempotently. A corrupt
|
|
97
|
+
// file is left untouched (never clobber a hand-edited config); an absent file is left absent (the
|
|
98
|
+
// managed overwrite lays the wired settings — the migration only backfills a hand-edited one).
|
|
99
|
+
const settingsPath = path.join(target, '.claude', 'settings.json');
|
|
100
|
+
if (fs.existsSync(settingsPath)) {
|
|
101
|
+
let cfg = null;
|
|
102
|
+
try {
|
|
103
|
+
cfg = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
104
|
+
} catch {
|
|
105
|
+
cfg = null; // hand-corrupted operator file → never clobber, never crash
|
|
106
|
+
}
|
|
107
|
+
if (cfg && typeof cfg === 'object' && wireSessionEndSpawn(cfg)) {
|
|
108
|
+
fs.writeFileSync(settingsPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 3. seed memory.config.json if absent (the slice-02 default). Copied from THIS package's payload so
|
|
113
|
+
// the seeded shape can never drift from what a fresh install ships. Already present ⇒ preserved
|
|
114
|
+
// (it is a seeded, operator-owned file). A missing payload source is swallowed (best-effort).
|
|
115
|
+
const cfgPath = path.join(target, '.wrxn', 'memory.config.json');
|
|
116
|
+
if (!fs.existsSync(cfgPath)) {
|
|
117
|
+
const seedSrc = path.join(__dirname, '..', 'payload', '.wrxn', 'memory.config.json');
|
|
118
|
+
try {
|
|
119
|
+
const seed = fs.readFileSync(seedSrc, 'utf8');
|
|
120
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
121
|
+
fs.writeFileSync(cfgPath, seed);
|
|
122
|
+
} catch {
|
|
123
|
+
// no payload seed reachable → skip (the managed/seeded update path lays it anyway)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 4. remove the stale `_slots/current-focus.md` focus slot (the slot + its set-focus op are dropped).
|
|
128
|
+
// Only the slot PAGE goes — the empty `_slots` tier dir + its gitkeep stay (the tier is retained).
|
|
129
|
+
// force = an absent slot is a no-op.
|
|
130
|
+
fs.rmSync(path.join(target, '.wrxn', 'wiki', '_slots', 'current-focus.md'), { force: true });
|
|
131
|
+
|
|
132
|
+
// 5. gitignore backfill — add the `.env` secret + continuity runtime-temp lines (each at most once).
|
|
133
|
+
// Closes the gap for installs created before slice-02 added `.env` / before the synth wrote temps.
|
|
134
|
+
for (const line of GITIGNORE_LINES) ensureGitignoreLine(target, line);
|
|
135
|
+
},
|
|
136
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gcunharodrigues/wrxn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "WRXN Kernel — installable AI operating system. Two profiles (project | workspace), pull-based updates, managed/seeded/state file classes.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"wrxn": "bin/wrxn.cjs"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// WRXN memory-synth-spawn — the SessionEnd hook that launches the background synthesizer (auto-memory-03).
|
|
5
|
+
// SessionEnd is otherwise unwired, so this is its sole hook. Its ONLY job: return {} immediately and
|
|
6
|
+
// spawn memory-synth.cjs DETACHED, so closing a session is NEVER blocked by summarization (PRD story 16).
|
|
7
|
+
//
|
|
8
|
+
// Recursion guard (PRD story 17): the synth runs `claude -p`, whose own session fires SessionEnd again —
|
|
9
|
+
// a SessionEnd→claude→SessionEnd fork-bomb. The synth sets WRXN_MEMORY_SYNTH=1 on every engine spawn;
|
|
10
|
+
// this hook no-ops (spawns nothing, writes no markers) when it sees that sentinel set.
|
|
11
|
+
//
|
|
12
|
+
// Before launching, it stashes the SessionEnd payload as the `.pending` marker and writes a
|
|
13
|
+
// `.pending-handoff` marker under .wrxn/continuity/ — both so SessionStart can detect an in-flight synth
|
|
14
|
+
// (and hold on the handoff marker) even before the detached child has done anything. The synth clears
|
|
15
|
+
// them on exit (memory-synth.cjs), so the markers describe "a synth is running for this session".
|
|
16
|
+
//
|
|
17
|
+
// Self-contained: ships into installs, MUST NOT import the kernel lib (node stdlib only).
|
|
18
|
+
// Fail-open: any fault emits {} (no spawn) — the hook NEVER blocks a session closing.
|
|
19
|
+
//
|
|
20
|
+
// Contract: SessionEnd event JSON on stdin → {} on stdout (exit 0).
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { spawn } = require('child_process');
|
|
25
|
+
|
|
26
|
+
const SENTINEL = 'WRXN_MEMORY_SYNTH'; // recursion guard (mirrors memory-synth.cjs's SENTINEL).
|
|
27
|
+
|
|
28
|
+
function findInstallRoot(startDir) {
|
|
29
|
+
let dir = startDir || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
30
|
+
for (let i = 0; i < 12; i++) {
|
|
31
|
+
if (fs.existsSync(path.join(dir, 'wrxn.install.json'))) return dir;
|
|
32
|
+
const up = path.dirname(dir);
|
|
33
|
+
if (up === dir) break;
|
|
34
|
+
dir = up;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The testable core. Given the SessionEnd payload, the install root, the env (for the recursion guard),
|
|
41
|
+
* and an injectable `spawn`, it stashes the payload + writes the pending markers and launches the synth
|
|
42
|
+
* detached — returning {} immediately. With the recursion sentinel set it no-ops (spawns nothing, writes
|
|
43
|
+
* no markers). Fail-open: any fault → {} (session close is never blocked).
|
|
44
|
+
* @param {{ payload:object, root:string, env:object, spawn:Function }} opts
|
|
45
|
+
* @returns {object} the hook envelope — always {}
|
|
46
|
+
*/
|
|
47
|
+
function run({ payload, root, env = process.env, spawn: spawnFn = spawn }) {
|
|
48
|
+
try {
|
|
49
|
+
if (env && env[SENTINEL]) return {}; // inside a synth-spawned session — do not recurse.
|
|
50
|
+
if (!root) return {};
|
|
51
|
+
|
|
52
|
+
const dir = path.join(root, '.wrxn', 'continuity');
|
|
53
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
54
|
+
|
|
55
|
+
// Stash the payload as .pending (the synth reads it for transcript_path) and raise the handoff gate.
|
|
56
|
+
fs.writeFileSync(path.join(dir, '.pending'), JSON.stringify(payload || {}));
|
|
57
|
+
fs.writeFileSync(path.join(dir, '.pending-handoff'), String(Date.now()));
|
|
58
|
+
|
|
59
|
+
// Launch the synth detached: stdio ignored + unref() so the parent's event loop never waits on it.
|
|
60
|
+
const synth = path.join(root, '.wrxn', 'memory-synth.cjs');
|
|
61
|
+
const child = spawnFn('node', [synth, '--from-spawn', '--root', root], {
|
|
62
|
+
detached: true,
|
|
63
|
+
stdio: 'ignore',
|
|
64
|
+
env: { ...env, [SENTINEL]: '1' },
|
|
65
|
+
});
|
|
66
|
+
if (child && typeof child.unref === 'function') child.unref();
|
|
67
|
+
} catch {
|
|
68
|
+
// fail-open: never block session close.
|
|
69
|
+
}
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function main() {
|
|
74
|
+
let consumed = '';
|
|
75
|
+
try {
|
|
76
|
+
consumed = fs.readFileSync(0, 'utf8');
|
|
77
|
+
} catch {
|
|
78
|
+
/* no stdin → nothing to synthesize */
|
|
79
|
+
}
|
|
80
|
+
let payload = {};
|
|
81
|
+
try {
|
|
82
|
+
payload = consumed.trim() ? JSON.parse(consumed) : {};
|
|
83
|
+
} catch {
|
|
84
|
+
payload = {};
|
|
85
|
+
}
|
|
86
|
+
const root = findInstallRoot(payload && payload.cwd);
|
|
87
|
+
const out = run({ payload, root, env: process.env, spawn });
|
|
88
|
+
process.stdout.write(JSON.stringify(out));
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (require.main === module) {
|
|
93
|
+
try {
|
|
94
|
+
main();
|
|
95
|
+
} catch {
|
|
96
|
+
process.stdout.write('{}');
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { run };
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
// WRXN session-start hook — the orientation surface (wrxn-kernel-10).
|
|
5
5
|
// SessionStart. Injects identity + resume as additionalContext so every new session opens
|
|
6
|
-
// oriented. The resume surfaces the
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// oriented. The resume surfaces the handoff baton at .wrxn/continuity/latest.md (single writer =
|
|
7
|
+
// the memory synth `memory-synth.cjs`, which writes the baton automatically on SessionEnd); absent
|
|
8
|
+
// a baton there is no prior handoff to resume. (The automatic episodic session-page fallback was
|
|
9
|
+
// retired with the session-capture subsystem in harvest-01.)
|
|
9
10
|
//
|
|
10
11
|
// Self-contained: ships into installs, MUST NOT import the kernel lib (node stdlib only).
|
|
11
12
|
// Fail-open: any fault emits {} (no orientation) — the hook NEVER blocks a session opening.
|
|
@@ -57,11 +58,76 @@ function identityLine(root) {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
// The deliberate handoff baton — the intent-carrying continuity slot. Single writer: the
|
|
60
|
-
// handoff
|
|
61
|
+
// auto-handoff synth (memory-synth.cjs, auto-memory-03). Read-only here; its presence is the resume.
|
|
61
62
|
function readBaton(root) {
|
|
62
63
|
return readFileOr(path.join(root, '.wrxn', 'continuity', 'latest.md'), null);
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
// ── the SessionEnd-synth hold (auto-memory-03) ──────────────────────────────────
|
|
67
|
+
// When a session ends, memory-synth-spawn.cjs raises a `.pending-handoff` marker and launches the synth
|
|
68
|
+
// detached; the synth writes the baton then clears the marker. So a back-to-back /clear could start
|
|
69
|
+
// BEFORE the fresh baton exists. We hold: poll the marker until it clears (synth done), bounded by a
|
|
70
|
+
// crash safety-cap so a SIGKILLed synth (marker never cleared) can never hang start forever. The
|
|
71
|
+
// poll-decision is pure and the loop takes an injected clock, so it is unit-tested with no wall sleep.
|
|
72
|
+
const HANDOFF_MARKER_REL = ['.wrxn', 'continuity', '.pending-handoff'];
|
|
73
|
+
const HOLD_CAP_MS = 60000; // crash safety-cap: never wait past this for an in-flight synth.
|
|
74
|
+
const HOLD_POLL_MS = 250; // poll cadence (the real sleep step; tests inject their own).
|
|
75
|
+
|
|
76
|
+
// A synchronous sleep for the real poll loop (the hook must finish before it emits; tests inject their
|
|
77
|
+
// own sleep and never reach this). Atomics.wait blocks the thread without a busy-spin (node stdlib).
|
|
78
|
+
function sleepMs() {
|
|
79
|
+
try {
|
|
80
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HOLD_POLL_MS);
|
|
81
|
+
} catch {
|
|
82
|
+
/* SharedArrayBuffer unavailable → degrade to no wait (the loop's wall-cap still bounds it) */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Pure poll-decision: wait vs proceed, from the marker's presence + age + the cap. No marker → proceed
|
|
88
|
+
* (no synth in flight). Marker present and younger than the cap → wait. Marker at/over the cap → proceed
|
|
89
|
+
* anyway (a crashed synth must not hang start). PURE.
|
|
90
|
+
* @param {{ markerExists:boolean, markerAgeMs:number, capMs:number }} s
|
|
91
|
+
* @returns {'wait'|'proceed'}
|
|
92
|
+
*/
|
|
93
|
+
function holdDecision({ markerExists, markerAgeMs, capMs }) {
|
|
94
|
+
if (!markerExists) return 'proceed';
|
|
95
|
+
return markerAgeMs >= capMs ? 'proceed' : 'wait';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Age (ms) of the handoff marker per the injected clock; Infinity if it is absent/unreadable. */
|
|
99
|
+
function markerAgeMs(root, now) {
|
|
100
|
+
try {
|
|
101
|
+
const st = fs.statSync(path.join(root, ...HANDOFF_MARKER_REL));
|
|
102
|
+
return Math.max(0, now() - st.mtimeMs);
|
|
103
|
+
} catch {
|
|
104
|
+
return Infinity; // absent → treated as "no marker" by the caller.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Hold until the in-flight synth clears the handoff marker, or the safety-cap elapses. The clock is
|
|
110
|
+
* injected (now()/sleep()) so the loop is deterministic in tests — NO wall-clock sleep here. Returns
|
|
111
|
+
* 'cleared' (marker gone → fresh baton ready) or 'capped' (gave up at the cap). Never throws.
|
|
112
|
+
* @param {{ root:string, capMs?:number, now?:Function, sleep?:Function }} opts
|
|
113
|
+
* @returns {'cleared'|'capped'}
|
|
114
|
+
*/
|
|
115
|
+
function holdForHandoff({ root, capMs = HOLD_CAP_MS, now = Date.now, sleep } = {}) {
|
|
116
|
+
const marker = path.join(root, ...HANDOFF_MARKER_REL);
|
|
117
|
+
const wait = sleep || (() => {}); // with no injected waiter the loop takes a single pass then caps (line below).
|
|
118
|
+
const started = now();
|
|
119
|
+
for (;;) {
|
|
120
|
+
const exists = fs.existsSync(marker);
|
|
121
|
+
if (!exists) return 'cleared';
|
|
122
|
+
const age = markerAgeMs(root, now);
|
|
123
|
+
if (holdDecision({ markerExists: true, markerAgeMs: age, capMs }) === 'proceed') return 'capped';
|
|
124
|
+
// also cap on wall-elapsed-since-entry, so a marker with a future/odd mtime still can't hang us.
|
|
125
|
+
if (now() - started >= capMs) return 'capped';
|
|
126
|
+
wait();
|
|
127
|
+
if (!sleep) return 'capped'; // no real waiter injected → don't spin; proceed.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
65
131
|
function main() {
|
|
66
132
|
let consumed = '';
|
|
67
133
|
try {
|
|
@@ -74,6 +140,14 @@ function main() {
|
|
|
74
140
|
const root = findInstallRoot();
|
|
75
141
|
if (!root) emit({});
|
|
76
142
|
|
|
143
|
+
// Hold for an in-flight SessionEnd synth (auto-memory-03) so a back-to-back /clear resumes on the
|
|
144
|
+
// FRESH baton, bounded by the crash safety-cap. Fail-open: any fault here must not block orientation.
|
|
145
|
+
try {
|
|
146
|
+
holdForHandoff({ root, sleep: sleepMs });
|
|
147
|
+
} catch {
|
|
148
|
+
/* never block the session opening on the hold */
|
|
149
|
+
}
|
|
150
|
+
|
|
77
151
|
const parts = [identityLine(root)];
|
|
78
152
|
|
|
79
153
|
const baton = readBaton(root);
|
|
@@ -91,8 +165,12 @@ function main() {
|
|
|
91
165
|
});
|
|
92
166
|
}
|
|
93
167
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
168
|
+
if (require.main === module) {
|
|
169
|
+
try {
|
|
170
|
+
main();
|
|
171
|
+
} catch {
|
|
172
|
+
emit({}); // fail-open: never block a session opening
|
|
173
|
+
}
|
|
98
174
|
}
|
|
175
|
+
|
|
176
|
+
module.exports = { holdDecision, holdForHandoff };
|
|
@@ -287,9 +287,8 @@ function handoffDirective(consumed, pct, hasDebt) {
|
|
|
287
287
|
'[HANDOFF REQUIRED]',
|
|
288
288
|
` Context is at ~${now}% of the model window (>= the ${thresh}% handoff threshold). NON-BLOCKING — do NOT stop work:`,
|
|
289
289
|
' 1. Finish the current request.',
|
|
290
|
-
' 2.
|
|
291
|
-
'
|
|
292
|
-
' Suggestion (optional, before step 2): run the dream skill to consolidate this session\'s durable learnings into wiki memory — a suggestion only; dream never auto-runs, it acts only when you invoke it.',
|
|
290
|
+
' 2. Tell the operator to /clear and open a fresh session. No manual step: the continuity baton writes automatically when this session ends (the memory synth) and injects on resume.',
|
|
291
|
+
' Suggestion (optional): the session also auto-consolidates its durable learnings into wiki memory on close (auto-dream); to consolidate explicitly or mid-session, invoke the dream skill — a suggestion only, never required.',
|
|
293
292
|
];
|
|
294
293
|
if (hasDebt) {
|
|
295
294
|
lines.push(' Then (optional, only because the last health-check found curation debt): run the harvest skill to review the flagged near-dups / decay-candidates / malformed pages — a suggestion only; harvest never auto-deletes, every change is proposed for your confirmation.');
|
|
@@ -162,40 +162,11 @@ recalls them automatically next session. Spot-check with a wiki query:
|
|
|
162
162
|
node .wrxn/wiki.cjs query "<a phrase from a page you just wrote>"
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
## Refreshing the focus slot
|
|
166
|
-
|
|
167
|
-
`_slots/current-focus.md` is the project's **durable standing focus** — a short statement of what the
|
|
168
|
-
project is centered on right now, recall-surfaced like any other page. It is the **lone updatable wiki
|
|
169
|
-
page**: every knowledge page is additive + dedup-skip, but the focus slot may be **overwritten in
|
|
170
|
-
place**.
|
|
171
|
-
|
|
172
|
-
This is **not** the knowledge-proposal loop — do not run a focus update through `check` / `stage` /
|
|
173
|
-
`commit` (those are for evidence-backed concept/decision/gotcha/rule pages). The slot has its **own op**:
|
|
174
|
-
|
|
175
|
-
1. Draft a short standing-focus statement (a few lines of markdown, body starting with `# `).
|
|
176
|
-
2. **Present it to the operator and wait for confirmation** — like every dream write.
|
|
177
|
-
3. On approval, write it via the dedicated op — it overwrites the slot in place:
|
|
178
|
-
|
|
179
|
-
```bash
|
|
180
|
-
node .wrxn/dream.cjs set-focus /tmp/dream-focus.json # { "title": "Current focus", "body": "# Current focus\n\n…" }
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
The focus slot is **gated** too: `set-focus` runs the anti-superstition negative filters and the
|
|
184
|
-
credential secret-scan over the focus body and **refuses** (writing nothing) if either fires. Redact
|
|
185
|
-
secrets and pin durable standing context, not a transient note.
|
|
186
|
-
|
|
187
|
-
**Continuity doctrine — do not cross these wires.** The focus slot is **disjoint** from the handoff
|
|
188
|
-
**baton** (`.wrxn/continuity/latest.md`): different path, different writer. `set-focus` NEVER reads or
|
|
189
|
-
writes the baton, and the **handoff** skill remains its sole writer. The baton is ephemeral cross-session
|
|
190
|
-
resume; the focus slot is durable standing context. Keeping their paths and writers separate is the
|
|
191
|
-
structural fix that stops a deliberate handoff from being clobbered.
|
|
192
|
-
|
|
193
165
|
## Boundaries
|
|
194
166
|
|
|
195
167
|
- **Current session only.** No transcript mining, no cross-session backlog.
|
|
196
|
-
- **Additive only
|
|
197
|
-
|
|
198
|
-
slot `_slots/current-focus.md`, which `set-focus` overwrites in place (see *Refreshing the focus slot*).
|
|
168
|
+
- **Additive only.** dream creates net-new knowledge pages; merging or refreshing an existing page is
|
|
169
|
+
out of scope (that is harvest, a later phase).
|
|
199
170
|
- **Never autonomous.** dream is a deliberate, attended, operator-confirmed skill — never a background
|
|
200
171
|
run, never a write without confirmation.
|
|
201
172
|
- **`_rules` ≠ SYNAPSE.** A `rule` page is *recalled knowledge* — the Brain surfaces it like a concept
|
|
@@ -62,9 +62,10 @@ marker records what was dropped. One budget, applied flat. See
|
|
|
62
62
|
|
|
63
63
|
When real consumed context reaches the handoff threshold (`HANDOFF_PCT`, default 0.40; override
|
|
64
64
|
`WRXN_HANDOFF_PCT`) of the model window, SYNAPSE appends a **non-blocking** `[HANDOFF REQUIRED]`
|
|
65
|
-
directive: finish the current request,
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
directive: finish the current request, then `/clear` and resume in a fresh session — the continuity
|
|
66
|
+
baton writes automatically on session end (the memory synth) and injects on resume, so there is no
|
|
67
|
+
manual step. It never refuses work. The math runs on real token usage (resident tokens from the
|
|
68
|
+
transcript ÷ the resolved model window), not an assumed window. See
|
|
68
69
|
[token budget & handoff](references/brackets.md).
|
|
69
70
|
|
|
70
71
|
## Output shape
|
|
@@ -89,8 +90,7 @@ Article I — Agent Authority (NON-NEGOTIABLE)
|
|
|
89
90
|
[HANDOFF REQUIRED]
|
|
90
91
|
Context is at ~42% of the model window (>= the 40% handoff threshold). NON-BLOCKING — do NOT stop work:
|
|
91
92
|
1. Finish the current request.
|
|
92
|
-
2.
|
|
93
|
-
3. Tell the operator to /clear and open a fresh session, where the baton injects on resume.
|
|
93
|
+
2. Tell the operator to /clear and open a fresh session. No manual step: the continuity baton writes automatically when this session ends (the memory synth) and injects on resume.
|
|
94
94
|
|
|
95
95
|
</synapse-rules>
|
|
96
96
|
```
|
|
@@ -30,8 +30,7 @@ When the real consumed context reaches the handoff threshold, SYNAPSE appends a
|
|
|
30
30
|
[HANDOFF REQUIRED]
|
|
31
31
|
Context is at ~42% of the model window (>= the 40% handoff threshold). NON-BLOCKING — do NOT stop work:
|
|
32
32
|
1. Finish the current request.
|
|
33
|
-
2.
|
|
34
|
-
3. Tell the operator to /clear and open a fresh session, where the baton injects on resume.
|
|
33
|
+
2. Tell the operator to /clear and open a fresh session. No manual step: the continuity baton writes automatically when this session ends (the memory synth) and injects on resume.
|
|
35
34
|
```
|
|
36
35
|
|
|
37
36
|
Like the constitution, the handoff directive is outside the budget — it is never trimmed.
|
|
@@ -79,8 +79,7 @@ Article I — Agent Authority (NON-NEGOTIABLE)
|
|
|
79
79
|
[HANDOFF REQUIRED]
|
|
80
80
|
Context is at ~42% of the model window (>= the 40% handoff threshold). NON-BLOCKING — do NOT stop work:
|
|
81
81
|
1. Finish the current request.
|
|
82
|
-
2.
|
|
83
|
-
3. Tell the operator to /clear and open a fresh session, where the baton injects on resume.
|
|
82
|
+
2. Tell the operator to /clear and open a fresh session. No manual step: the continuity baton writes automatically when this session ends (the memory synth) and injects on resume.
|
|
84
83
|
|
|
85
84
|
</synapse-rules>
|
|
86
85
|
```
|