@enderfga/claw-orchestrator 7.6.2 → 7.8.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/README.md +4 -4
- package/dist/src/acp-server.d.ts +28 -6
- package/dist/src/acp-server.js +108 -19
- package/dist/src/acp-server.js.map +1 -1
- package/dist/src/index.js +10 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/kernel/nodes/autoloop.d.ts +2 -0
- package/dist/src/kernel/nodes/autoloop.js +5 -0
- package/dist/src/kernel/nodes/autoloop.js.map +1 -1
- package/dist/src/persistent-codex-app-session.d.ts +2 -0
- package/dist/src/persistent-codex-app-session.js +11 -0
- package/dist/src/persistent-codex-app-session.js.map +1 -1
- package/dist/src/persistent-codex-session.d.ts +9 -0
- package/dist/src/persistent-codex-session.js +24 -0
- package/dist/src/persistent-codex-session.js.map +1 -1
- package/dist/src/persistent-session.d.ts +2 -0
- package/dist/src/persistent-session.js +26 -1
- package/dist/src/persistent-session.js.map +1 -1
- package/dist/src/run-ledger.d.ts +6 -0
- package/dist/src/run-ledger.js.map +1 -1
- package/dist/src/session-manager.d.ts +13 -0
- package/dist/src/session-manager.js +164 -47
- package/dist/src/session-manager.js.map +1 -1
- package/dist/src/types.d.ts +37 -0
- package/package.json +1 -1
- package/skills/references/acp.md +36 -11
- package/skills/references/autoloop.md +7 -1
- package/skills/references/claude-cli-tracking.md +3 -1
- package/skills/references/mcp.md +19 -0
- package/skills/references/multi-engine.md +8 -3
- package/skills/references/observability.md +2 -1
- package/skills/references/sessions.md +11 -0
- package/skills/references/tools.md +6 -2
|
@@ -36,7 +36,8 @@ function getPluginVersion() {
|
|
|
36
36
|
return 'unknown';
|
|
37
37
|
}
|
|
38
38
|
// ─── Persistence ─────────────────────────────────────────────────────────────
|
|
39
|
-
|
|
39
|
+
// CLAWO_SESSIONS_DIR relocates the file, so tests never touch the real one.
|
|
40
|
+
const PERSIST_DIR = process.env.CLAWO_SESSIONS_DIR || path.join(os.homedir(), '.openclaw');
|
|
40
41
|
const PERSIST_FILE = path.join(PERSIST_DIR, 'claude-sessions.json');
|
|
41
42
|
function loadPersistedSessions() {
|
|
42
43
|
try {
|
|
@@ -53,45 +54,50 @@ function loadPersistedSessions() {
|
|
|
53
54
|
return new Map();
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Write this process's changes into the shared session file.
|
|
59
|
+
*
|
|
60
|
+
* Several processes share the file — the server, the OpenClaw plugin, every
|
|
61
|
+
* `clawo-mcp` and `clawo acp` a host starts. Each used to write its whole
|
|
62
|
+
* in-memory map back, so the last writer erased whatever the others had added
|
|
63
|
+
* since it loaded: a long-lived session's resume record could vanish because an
|
|
64
|
+
* unrelated process exited. Now only the names this process changed (`dirty`,
|
|
65
|
+
* set or deleted) are merged into what is on disk, under a lock, and the merged
|
|
66
|
+
* map is returned so the caller sees the other processes' sessions too.
|
|
67
|
+
*
|
|
68
|
+
* Returns undefined when the lock stays busy; `dirty` is left as it was, so
|
|
69
|
+
* the changes go out with the next save instead of being lost.
|
|
70
|
+
*/
|
|
71
|
+
function savePersistedSessions(mine, dirty, logger) {
|
|
72
|
+
const log = logger || createConsoleLogger('SessionManager');
|
|
58
73
|
try {
|
|
59
74
|
fs.mkdirSync(PERSIST_DIR, { recursive: true });
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
75
|
+
const result = withFileLock(`${PERSIST_FILE}.lock`, () => {
|
|
76
|
+
const merged = loadPersistedSessions();
|
|
77
|
+
for (const name of dirty) {
|
|
78
|
+
const entry = mine.get(name);
|
|
79
|
+
if (entry)
|
|
80
|
+
merged.set(name, entry);
|
|
81
|
+
else
|
|
82
|
+
merged.delete(name);
|
|
83
|
+
}
|
|
84
|
+
const tmp = `${PERSIST_FILE}.${process.pid}.tmp`;
|
|
85
|
+
fs.writeFileSync(tmp, JSON.stringify(Array.from(merged.values()), null, 2));
|
|
86
|
+
fs.renameSync(tmp, PERSIST_FILE);
|
|
87
|
+
return merged;
|
|
88
|
+
});
|
|
89
|
+
if (!result.ok) {
|
|
90
|
+
log.warn('Session file busy; changes kept for the next save');
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
dirty.clear();
|
|
94
|
+
return result.value;
|
|
64
95
|
}
|
|
65
96
|
catch (err) {
|
|
66
|
-
|
|
97
|
+
log.warn('Failed to persist sessions:', err.message);
|
|
98
|
+
return undefined;
|
|
67
99
|
}
|
|
68
100
|
}
|
|
69
|
-
// Async version for hot-path (sendMessage, TTL cleanup)
|
|
70
|
-
function savePersistedSessionsAsync(sessions, logger) {
|
|
71
|
-
const log = logger || createConsoleLogger('SessionManager');
|
|
72
|
-
const arr = Array.from(sessions.values());
|
|
73
|
-
const tmp = PERSIST_FILE + '.tmp';
|
|
74
|
-
fs.mkdir(PERSIST_DIR, { recursive: true }, (mkdirErr) => {
|
|
75
|
-
if (mkdirErr) {
|
|
76
|
-
log.error('Failed to create persist dir:', mkdirErr.message);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
fs.writeFile(tmp, JSON.stringify(arr, null, 2), (writeErr) => {
|
|
80
|
-
if (writeErr) {
|
|
81
|
-
log.error('Failed to write session file:', writeErr.message);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
fs.rename(tmp, PERSIST_FILE, (renameErr) => {
|
|
85
|
-
if (renameErr) {
|
|
86
|
-
log.error('Failed to rename session file:', renameErr.message);
|
|
87
|
-
// Clean up orphan tmp file
|
|
88
|
-
fs.unlink(tmp, () => { });
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
// Debounce helper — coalesces rapid writes into one
|
|
95
101
|
function makeDebounced(fn, ms) {
|
|
96
102
|
let timer = null;
|
|
97
103
|
return () => {
|
|
@@ -117,6 +123,7 @@ import { runContract } from './verify/runner.js';
|
|
|
117
123
|
import { evidenceDir, listEvidence, readEvidence, writeEvidence } from './verify/evidence.js';
|
|
118
124
|
import { annotateVerdicts, appendRunRow, readRunLedger, summarizeRuns, } from './run-ledger.js';
|
|
119
125
|
import { checkBudget, isBudgetExceeded } from './budget.js';
|
|
126
|
+
import { withFileLock } from './kernel/file-lock.js';
|
|
120
127
|
import { InboxManager } from './inbox-manager.js';
|
|
121
128
|
import { sanitizeCwd, validateName } from './validation.js';
|
|
122
129
|
import { PersistentClaudeSession } from './persistent-session.js';
|
|
@@ -327,6 +334,13 @@ function validateAutoloopEffort(role, effort) {
|
|
|
327
334
|
* engine emits a result event of its own shape, so anything that is not an
|
|
328
335
|
* array of objects naming a tool is ignored rather than trusted.
|
|
329
336
|
*/
|
|
337
|
+
/** Images an engine attached to its result event as `generated_images`. */
|
|
338
|
+
function readGeneratedImages(evt) {
|
|
339
|
+
const raw = evt?.generated_images;
|
|
340
|
+
if (!Array.isArray(raw))
|
|
341
|
+
return [];
|
|
342
|
+
return raw.filter((i) => !!i && typeof i === 'object' && typeof i.path === 'string');
|
|
343
|
+
}
|
|
330
344
|
function readPermissionDenials(evt) {
|
|
331
345
|
const raw = evt?.permission_denials;
|
|
332
346
|
if (!Array.isArray(raw))
|
|
@@ -392,7 +406,7 @@ export class SessionManager {
|
|
|
392
406
|
// Clean up orphaned child processes from a previous unclean exit
|
|
393
407
|
this._cleanupOrphanedPids();
|
|
394
408
|
// Debounced async writer — at most one write per 5 seconds on hot paths
|
|
395
|
-
this._debouncedSave = makeDebounced(() =>
|
|
409
|
+
this._debouncedSave = makeDebounced(() => this._savePersisted(), DEBOUNCED_SAVE_MS);
|
|
396
410
|
// Start TTL cleanup timer
|
|
397
411
|
this.cleanupTimer = setInterval(() => this._cleanupIdleSessions(), CLEANUP_INTERVAL_MS);
|
|
398
412
|
}
|
|
@@ -445,6 +459,10 @@ export class SessionManager {
|
|
|
445
459
|
// ─── Session Lifecycle ─────────────────────────────────────────────────
|
|
446
460
|
async startSession(config) {
|
|
447
461
|
const name = config.name || `session-${Date.now()}`;
|
|
462
|
+
const compactAt = config.autoCompactPercent;
|
|
463
|
+
if (compactAt !== undefined && !(Number.isFinite(compactAt) && compactAt >= 1 && compactAt <= 99)) {
|
|
464
|
+
throw new Error('autoCompactPercent must be a number from 1 to 99');
|
|
465
|
+
}
|
|
448
466
|
// Check pending first — a concurrent caller may have already started creation
|
|
449
467
|
const pending = this._pendingSessions.get(name);
|
|
450
468
|
if (pending)
|
|
@@ -518,6 +536,8 @@ export class SessionManager {
|
|
|
518
536
|
effort: config.effort || this.pluginConfig.defaultEffort,
|
|
519
537
|
model: config.model || persisted?.model || this.pluginConfig.defaultModel,
|
|
520
538
|
sandboxMode: config.sandboxMode ?? persisted?.sandboxMode,
|
|
539
|
+
autoCompactPercent: config.autoCompactPercent ?? persisted?.autoCompactPercent,
|
|
540
|
+
chrome: config.chrome ?? persisted?.chrome,
|
|
521
541
|
...config,
|
|
522
542
|
...(resumeId ? { resumeSessionId: resumeId } : {}),
|
|
523
543
|
};
|
|
@@ -617,6 +637,20 @@ export class SessionManager {
|
|
|
617
637
|
session: name,
|
|
618
638
|
engine: managed.config.engine || 'claude',
|
|
619
639
|
});
|
|
640
|
+
// Compact before the send rather than after the previous reply, so the
|
|
641
|
+
// caller is never kept waiting for it, and inside the send chain, so no
|
|
642
|
+
// other send can interleave. Best effort: a failed or unsupported compact
|
|
643
|
+
// must not cost the caller their turn.
|
|
644
|
+
const compactAt = managed.config.autoCompactPercent;
|
|
645
|
+
if (compactAt && managed.session.getStats().contextPercent >= compactAt) {
|
|
646
|
+
try {
|
|
647
|
+
await managed.session.compact();
|
|
648
|
+
this.logger.info(`[SessionManager] ${name}: context at or above ${compactAt}% — compacted before send`);
|
|
649
|
+
}
|
|
650
|
+
catch (err) {
|
|
651
|
+
this.logger.warn(`[SessionManager] ${name}: auto-compact failed: ${err.message}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
620
654
|
const sendOpts = {
|
|
621
655
|
waitForComplete: true,
|
|
622
656
|
timeout: options.timeout || TURN_TIMEOUT_MS,
|
|
@@ -687,6 +721,7 @@ export class SessionManager {
|
|
|
687
721
|
// through. The result event was dropped at this line, and it is the
|
|
688
722
|
// only record of a blocked call: the turn itself still reports success.
|
|
689
723
|
const permissionDenials = readPermissionDenials(evt);
|
|
724
|
+
const images = readGeneratedImages(evt);
|
|
690
725
|
// The record holds what the caller said, never the replayed history in
|
|
691
726
|
// front of it — a second handoff would otherwise nest one inside the other.
|
|
692
727
|
managed.transcript ??= newTranscript();
|
|
@@ -701,6 +736,7 @@ export class SessionManager {
|
|
|
701
736
|
error: turnError,
|
|
702
737
|
events: [],
|
|
703
738
|
...(permissionDenials.length ? { permissionDenials } : {}),
|
|
739
|
+
...(images.length ? { images } : {}),
|
|
704
740
|
};
|
|
705
741
|
}
|
|
706
742
|
return { output: '', sessionId: this._managedResumeId(managed), events: [] };
|
|
@@ -757,6 +793,7 @@ export class SessionManager {
|
|
|
757
793
|
sandboxMode: source.config.sandboxMode,
|
|
758
794
|
effort: source.config.effort,
|
|
759
795
|
maxBudgetUsd: source.config.maxBudgetUsd,
|
|
796
|
+
autoCompactPercent: source.config.autoCompactPercent,
|
|
760
797
|
systemPrompt: source.config.systemPrompt,
|
|
761
798
|
appendSystemPrompt: source.config.appendSystemPrompt,
|
|
762
799
|
addDir: source.config.addDir,
|
|
@@ -813,6 +850,7 @@ export class SessionManager {
|
|
|
813
850
|
toolCalls: 0,
|
|
814
851
|
toolErrors: 0,
|
|
815
852
|
costUsd: 0,
|
|
853
|
+
refusalFallbacks: 0,
|
|
816
854
|
};
|
|
817
855
|
try {
|
|
818
856
|
const st = managed.session.getStats();
|
|
@@ -825,6 +863,7 @@ export class SessionManager {
|
|
|
825
863
|
toolCalls: st.toolCalls || 0,
|
|
826
864
|
toolErrors: st.toolErrors || 0,
|
|
827
865
|
costUsd: this._spentUsd(managed),
|
|
866
|
+
refusalFallbacks: st.refusalFallbacks || 0,
|
|
828
867
|
};
|
|
829
868
|
}
|
|
830
869
|
catch {
|
|
@@ -865,12 +904,21 @@ export class SessionManager {
|
|
|
865
904
|
ok: !error && (after.turns > before.turns ? after.turnsSucceeded > before.turnsSucceeded : true),
|
|
866
905
|
};
|
|
867
906
|
// Fall back to the engine's own reported model, so a session started
|
|
868
|
-
// without an explicit `model` still records what actually answered.
|
|
869
|
-
|
|
907
|
+
// without an explicit `model` still records what actually answered. An
|
|
908
|
+
// alias (`opus[1m]`) is the CLI's to resolve, so what it reports wins there.
|
|
909
|
+
const configured = managed.config.resolvedModel || managed.config.model;
|
|
910
|
+
const bare = configured?.replace(/\[1m\]$/i, '');
|
|
911
|
+
const isAlias = !!bare && resolveAlias(bare) !== bare;
|
|
912
|
+
const model = (isAlias ? this._reportedModel(managed) : undefined) || configured || this._reportedModel(managed);
|
|
870
913
|
if (model)
|
|
871
914
|
row.model = model;
|
|
872
915
|
if (error)
|
|
873
916
|
row.error = error.slice(0, 500);
|
|
917
|
+
if (after.refusalFallbacks > before.refusalFallbacks) {
|
|
918
|
+
const fallback = managed.session.getStats().lastRefusalFallback;
|
|
919
|
+
if (fallback)
|
|
920
|
+
row.refusalFallback = fallback;
|
|
921
|
+
}
|
|
874
922
|
if (parent)
|
|
875
923
|
row.parent = parent;
|
|
876
924
|
if (dims.nodeKind)
|
|
@@ -1103,8 +1151,8 @@ export class SessionManager {
|
|
|
1103
1151
|
// Callers that want the session resumable (autoloop terminate that
|
|
1104
1152
|
// should still allow /autoloop/<id>/resume to reattach the Planner's
|
|
1105
1153
|
// Claude conversation) pass keepPersisted: true.
|
|
1106
|
-
this.
|
|
1107
|
-
|
|
1154
|
+
this._forgetPersisted(name);
|
|
1155
|
+
this._savePersisted();
|
|
1108
1156
|
}
|
|
1109
1157
|
}
|
|
1110
1158
|
hasSession(name) {
|
|
@@ -1505,7 +1553,7 @@ export class SessionManager {
|
|
|
1505
1553
|
this._proxyPort = null;
|
|
1506
1554
|
}
|
|
1507
1555
|
// Persist final state (TTL-expired sessions already removed by cleanup)
|
|
1508
|
-
|
|
1556
|
+
this._savePersisted();
|
|
1509
1557
|
}
|
|
1510
1558
|
// ─── Codex /goal helpers (codex-app engine only) ─────────────────────
|
|
1511
1559
|
/**
|
|
@@ -1940,16 +1988,32 @@ export class SessionManager {
|
|
|
1940
1988
|
});
|
|
1941
1989
|
}
|
|
1942
1990
|
// ─── Private ───────────────────────────────────────────────────────────
|
|
1991
|
+
/** Names this process has set or deleted since its last save; see savePersistedSessions. */
|
|
1992
|
+
_persistDirty = new Set();
|
|
1993
|
+
_forgetPersisted(name) {
|
|
1994
|
+
this.persistedSessions.delete(name);
|
|
1995
|
+
this._persistDirty.add(name);
|
|
1996
|
+
}
|
|
1997
|
+
/** Merge this process's changes into the shared file and adopt the merged view. */
|
|
1998
|
+
_savePersisted() {
|
|
1999
|
+
if (this._persistDirty.size === 0)
|
|
2000
|
+
return;
|
|
2001
|
+
const merged = savePersistedSessions(this.persistedSessions, this._persistDirty, this.logger);
|
|
2002
|
+
if (merged)
|
|
2003
|
+
this.persistedSessions = merged;
|
|
2004
|
+
}
|
|
1943
2005
|
_persistSession(name, managed) {
|
|
1944
2006
|
const resumeSessionId = this._managedResumeId(managed);
|
|
1945
2007
|
if (!resumeSessionId) {
|
|
1946
|
-
if (managed.config.engine === 'agy' && this.persistedSessions.
|
|
2008
|
+
if (managed.config.engine === 'agy' && this.persistedSessions.has(name)) {
|
|
2009
|
+
this._forgetPersisted(name);
|
|
1947
2010
|
this._debouncedSave();
|
|
1948
2011
|
}
|
|
1949
2012
|
return;
|
|
1950
2013
|
}
|
|
1951
2014
|
managed.claudeSessionId = resumeSessionId;
|
|
1952
2015
|
const existing = this.persistedSessions.get(name);
|
|
2016
|
+
this._persistDirty.add(name);
|
|
1953
2017
|
this.persistedSessions.set(name, {
|
|
1954
2018
|
name,
|
|
1955
2019
|
claudeSessionId: resumeSessionId,
|
|
@@ -1957,6 +2021,8 @@ export class SessionManager {
|
|
|
1957
2021
|
model: managed.config.resolvedModel || managed.config.model,
|
|
1958
2022
|
engine: managed.config.engine,
|
|
1959
2023
|
sandboxMode: managed.config.sandboxMode,
|
|
2024
|
+
autoCompactPercent: managed.config.autoCompactPercent,
|
|
2025
|
+
chrome: managed.config.chrome,
|
|
1960
2026
|
originalCreated: existing?.originalCreated || managed.created,
|
|
1961
2027
|
lastResumed: new Date().toISOString(),
|
|
1962
2028
|
lastActivity: managed.lastActivity,
|
|
@@ -2804,6 +2870,7 @@ export class SessionManager {
|
|
|
2804
2870
|
// this inside the startup try makes an append failure follow the same
|
|
2805
2871
|
// cleanup path as any other failed resume.
|
|
2806
2872
|
opts._commitTimeoutMigration?.();
|
|
2873
|
+
await this._recoverCommittedCoderIteration(opts.workspace, ledgerDir, runId, runner, dispatcher);
|
|
2807
2874
|
}
|
|
2808
2875
|
catch (err) {
|
|
2809
2876
|
try {
|
|
@@ -2819,6 +2886,57 @@ export class SessionManager {
|
|
|
2819
2886
|
}
|
|
2820
2887
|
return { runner, dispatcher, ledgerDir, pushPolicy };
|
|
2821
2888
|
}
|
|
2889
|
+
/**
|
|
2890
|
+
* If the durable ledger has a committed Coder iteration with no Reviewer
|
|
2891
|
+
* verdict, and HEAD still names that iter, deliver it to Reviewer once.
|
|
2892
|
+
* Git failure or a subject mismatch leaves this resume as a Planner-only boot.
|
|
2893
|
+
*/
|
|
2894
|
+
async _recoverCommittedCoderIteration(workspace, ledgerDir, runId, runner, dispatcher) {
|
|
2895
|
+
const iterRoot = path.join(ledgerDir, 'iter');
|
|
2896
|
+
if (!fs.existsSync(iterRoot))
|
|
2897
|
+
return;
|
|
2898
|
+
const iters = fs
|
|
2899
|
+
.readdirSync(iterRoot)
|
|
2900
|
+
.filter((name) => /^\d+$/.test(name))
|
|
2901
|
+
.map((name) => Number(name));
|
|
2902
|
+
if (iters.length === 0)
|
|
2903
|
+
return;
|
|
2904
|
+
const n = Math.max(...iters);
|
|
2905
|
+
const artifacts = ['directive.json', 'eval_output.json', 'coder_summary.txt', 'diff.patch'];
|
|
2906
|
+
const iterDir = path.join(iterRoot, String(n));
|
|
2907
|
+
if (artifacts.some((name) => !fs.existsSync(path.join(iterDir, name))))
|
|
2908
|
+
return;
|
|
2909
|
+
if (fs.existsSync(path.join(iterDir, 'verdict.json')))
|
|
2910
|
+
return;
|
|
2911
|
+
for (let i = 0; i < n; i++) {
|
|
2912
|
+
if (!fs.existsSync(path.join(iterRoot, String(i), 'verdict.json')))
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
let subject;
|
|
2916
|
+
try {
|
|
2917
|
+
subject = execFileSync('git', ['-C', workspace, 'log', '-1', '--format=%s'], {
|
|
2918
|
+
encoding: 'utf8',
|
|
2919
|
+
}).trim();
|
|
2920
|
+
}
|
|
2921
|
+
catch {
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
if (!subject.startsWith(`autoloop/iter-${n}:`))
|
|
2925
|
+
return;
|
|
2926
|
+
this.logger.info?.(`[autoloop/${runId}] recovering committed iter ${n} into Reviewer`);
|
|
2927
|
+
await dispatcher.spawnSubagents();
|
|
2928
|
+
runner.markSubagentsSpawned();
|
|
2929
|
+
runner.state.iter = n;
|
|
2930
|
+
void runner
|
|
2931
|
+
.send(AutoloopMsg.reviewRequest(n, {
|
|
2932
|
+
iter: n,
|
|
2933
|
+
ledger_path: ledgerDir,
|
|
2934
|
+
prior_metrics: runner.state.metric_history.slice(-10),
|
|
2935
|
+
}))
|
|
2936
|
+
.catch((err) => {
|
|
2937
|
+
this.logger.warn?.(`[autoloop/${runId}] recovered review_request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2822
2940
|
/**
|
|
2823
2941
|
* Start a v2 autoloop in chat mode. Creates the Planner persistent session,
|
|
2824
2942
|
* returns the run handle. Coder/Reviewer are NOT started until S3's
|
|
@@ -3233,10 +3351,9 @@ export class SessionManager {
|
|
|
3233
3351
|
catch {
|
|
3234
3352
|
/* session not in memory — fine */
|
|
3235
3353
|
}
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
this.
|
|
3239
|
-
savePersistedSessions(this.persistedSessions, this.logger);
|
|
3354
|
+
for (const role of ['planner', 'coder', 'reviewer'])
|
|
3355
|
+
this._forgetPersisted(`autoloop-${runId}-${role}`);
|
|
3356
|
+
this._savePersisted();
|
|
3240
3357
|
}
|
|
3241
3358
|
// No registry to scrub: the run record IS the registry, and removing it is
|
|
3242
3359
|
// the delete. The ledger directory under tasks/<runId>/ is deliberately left
|
|
@@ -3286,12 +3403,12 @@ export class SessionManager {
|
|
|
3286
3403
|
let pruned = false;
|
|
3287
3404
|
for (const [name, entry] of this.persistedSessions) {
|
|
3288
3405
|
if (now - entry.lastActivity > PERSIST_DISK_TTL_MS) {
|
|
3289
|
-
this.
|
|
3406
|
+
this._forgetPersisted(name);
|
|
3290
3407
|
pruned = true;
|
|
3291
3408
|
}
|
|
3292
3409
|
}
|
|
3293
3410
|
if (pruned)
|
|
3294
|
-
|
|
3411
|
+
this._debouncedSave();
|
|
3295
3412
|
}
|
|
3296
3413
|
}
|
|
3297
3414
|
//# sourceMappingURL=session-manager.js.map
|