@a1hvdy/cc-openclaw 0.27.9 → 0.27.11
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/src/channels/telegram-mirror/card-renderer.js +8 -2
- package/dist/src/engines/persistent-session.js +10 -3
- package/dist/src/lib/html-render.js +13 -0
- package/dist/src/models.js +2 -2
- package/dist/src/openai-compat/autonomy-rule.d.ts +26 -0
- package/dist/src/openai-compat/autonomy-rule.js +56 -0
- package/dist/src/openai-compat/openai-compat.js +21 -5
- package/dist/src/session/persisted-sessions.d.ts +15 -0
- package/dist/src/session/persisted-sessions.js +16 -0
- package/dist/src/session/session-manager.d.ts +1 -0
- package/dist/src/session/session-manager.js +27 -2
- package/package.json +1 -1
|
@@ -346,9 +346,12 @@ export function renderTurn(turn, meta) {
|
|
|
346
346
|
push(`<b>${escapeHtml(status)}</b>`);
|
|
347
347
|
// v0.26.2 M2 — meter row (context % + quota % + reset). Same no-fake-data
|
|
348
348
|
// gating: omitted unless real values are present. HTML-escaped for safety.
|
|
349
|
+
// v0.27.10 — wrap the meter row in <code> so the bars read as one cohesive
|
|
350
|
+
// monospace status widget (ctx ▓▓░ 6% · use …) instead of loose plain text
|
|
351
|
+
// sitting under the bold header — part of the "solid HTML" cleanup.
|
|
349
352
|
const meters = renderMeters(meta);
|
|
350
353
|
if (meters)
|
|
351
|
-
push(escapeHtml(meters));
|
|
354
|
+
push(`<code>${escapeHtml(meters)}</code>`);
|
|
352
355
|
// v0.26.4 styling — divider between the status/telemetry block and the
|
|
353
356
|
// activity block (only when a status block was actually rendered). The
|
|
354
357
|
// heavy-bar glyph is not HTML-significant, so it needs no escaping.
|
|
@@ -362,7 +365,10 @@ export function renderTurn(turn, meta) {
|
|
|
362
365
|
: turn.state === 'done'
|
|
363
366
|
? '✓ Done'
|
|
364
367
|
: '▶ Working';
|
|
365
|
-
|
|
368
|
+
// v0.27.10 — bold the turn-status header so it anchors the activity block as a
|
|
369
|
+
// styled heading rather than a plain line (cohesion pass). failReason is already
|
|
370
|
+
// escapeHtml'd above; ✓ Done / ▶ Working are literal-safe.
|
|
371
|
+
push(`<b>${header}</b>`);
|
|
366
372
|
if (turn.toolCalls.length > 0) {
|
|
367
373
|
// Build tool ENTRIES (tool line + its optional <pre><code> result block)
|
|
368
374
|
// newest-first, keeping what fits under budget; the older overflow collapses
|
|
@@ -611,10 +611,17 @@ export class PersistentClaudeSession extends EventEmitter {
|
|
|
611
611
|
this.stats.tokensOut += usage.output_tokens || 0;
|
|
612
612
|
this.stats.cachedTokens += usage.cache_read_input_tokens || 0;
|
|
613
613
|
// v0.6.0: track actual per-turn context occupancy for the contextPercent
|
|
614
|
-
// calc.
|
|
615
|
-
//
|
|
614
|
+
// calc. v0.27.10: include cache_creation_input_tokens. The full prompt
|
|
615
|
+
// sent each turn = input (new uncached) + cache_read (cached prefix
|
|
616
|
+
// re-read) + cache_creation (newly written to cache). Summing ALL THREE
|
|
617
|
+
// yields the true current context-window occupancy, stable across cache
|
|
618
|
+
// hit/miss. Omitting cache_creation made the meter swing wildly (cold
|
|
619
|
+
// cache → everything is creation, uncounted → ~6%; warm cache → big
|
|
620
|
+
// read → 63%+) — the exact 6%→63%→100% jitter A1 flagged 2026-05-22.
|
|
616
621
|
this.stats.lastTurnContextTokens =
|
|
617
|
-
(usage.input_tokens || 0) +
|
|
622
|
+
(usage.input_tokens || 0) +
|
|
623
|
+
(usage.cache_read_input_tokens || 0) +
|
|
624
|
+
(usage.cache_creation_input_tokens || 0);
|
|
618
625
|
this._updateCost();
|
|
619
626
|
}
|
|
620
627
|
this.emit(SESSION_EVENT.RESULT, event);
|
|
@@ -223,6 +223,19 @@ export function markdownToHtml(input) {
|
|
|
223
223
|
// glyph isn't HTML-significant, so it survives the escape below. Uses [ \t]
|
|
224
224
|
// (not \s) so it never consumes the line break.
|
|
225
225
|
text = text.replace(/^([ \t]*)[-*+][ \t]+/gm, (_m, indent) => `${indent}• `);
|
|
226
|
+
// v0.27.10 — strip dangling inline-markdown delimiters left by PARTIAL
|
|
227
|
+
// streaming text. Every CLOSED span (```fence```, `code`, **bold**, ~~strike~~)
|
|
228
|
+
// was already lifted out to a NUL placeholder above; whatever **, ~~ or ` is
|
|
229
|
+
// still here is an UNTERMINATED marker at the live-stream frontier (e.g. the
|
|
230
|
+
// card edited mid-token while the model is still typing "…and the **bold"). Left
|
|
231
|
+
// in place it renders as a raw symbol next to formatted text — the "normal text
|
|
232
|
+
// mixed between HTML" A1 flagged 2026-05-22. Remove the markers, keep the words.
|
|
233
|
+
// Finalized (balanced) text has none left, so this is a no-op there. `*`/`_` are
|
|
234
|
+
// deliberately NOT stripped (too easily literal math / snake_case → false hits).
|
|
235
|
+
text = text
|
|
236
|
+
.replace(/\*\*/g, '')
|
|
237
|
+
.replace(/~~/g, '')
|
|
238
|
+
.replace(/`/g, '');
|
|
226
239
|
text = escapeHtml(text);
|
|
227
240
|
text = text.replace(/\x00CODEBLOCK(\d+)\x00/g, (_m, idx) => codeBlocks[Number(idx)]);
|
|
228
241
|
text = text.replace(/\x00TABLE(\d+)\x00/g, (_m, idx) => tables[Number(idx)]);
|
package/dist/src/models.js
CHANGED
|
@@ -219,7 +219,7 @@ export function resolveProvider(model) {
|
|
|
219
219
|
}
|
|
220
220
|
/** Get context window size for a model. Returns 200k default for unknown models. */
|
|
221
221
|
export function getContextWindow(model) {
|
|
222
|
-
const clean = model.replace(/^(anthropic|openai|openai-codex|google|gemini|cursor)\//g, '');
|
|
222
|
+
const clean = model.replace(/^(anthropic|openai|openai-codex|google|gemini|cursor|cc-openclaw)\//g, '');
|
|
223
223
|
const known = lookupModel(clean);
|
|
224
224
|
return known?.contextWindow ?? 200_000;
|
|
225
225
|
}
|
|
@@ -227,7 +227,7 @@ export function getContextWindow(model) {
|
|
|
227
227
|
export function getModelPricing(model, defaultModel = 'claude-sonnet-4-6') {
|
|
228
228
|
if (!model)
|
|
229
229
|
return lookupModel(defaultModel)?.pricing ?? { input: 0, output: 0 };
|
|
230
|
-
const clean = model.replace(/^(anthropic|openai|openai-codex|google|gemini|cursor)\//g, '');
|
|
230
|
+
const clean = model.replace(/^(anthropic|openai|openai-codex|google|gemini|cursor|cc-openclaw)\//g, '');
|
|
231
231
|
// Check overrides first
|
|
232
232
|
const override = _pricingOverrides.get(clean);
|
|
233
233
|
if (override)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTONOMY_RULE — "act, don't ask" posture injected into the Savvy chat path.
|
|
3
|
+
*
|
|
4
|
+
* Why: the openai-compat (Telegram) path does not reliably load the owner's
|
|
5
|
+
* CLAUDE.md "Execute, don't discuss" rules (tmpdir CWD), so the model reverts to
|
|
6
|
+
* Claude's base posture of asking on ambiguous forks. Combined with a Telegram
|
|
7
|
+
* AskUserQuestion round-trip that silently drops answers, this made Savvy stall
|
|
8
|
+
* and require many prods to finish one task. This rule restores the terminal-CLI
|
|
9
|
+
* posture: decide and execute, complete the whole task in one turn.
|
|
10
|
+
*
|
|
11
|
+
* Gated by CC_OPENCLAW_AUTONOMY_RULE (default on; set '0' to disable). Prepended
|
|
12
|
+
* at the same injection points as TTS_RULE in openai-compat.ts so it lands in
|
|
13
|
+
* both the REPLACE (--system-prompt) and APPEND (--append-system-prompt) paths.
|
|
14
|
+
*
|
|
15
|
+
* Pure-string constant — no I/O, no module state.
|
|
16
|
+
*/
|
|
17
|
+
export declare const AUTONOMY_RULE: string;
|
|
18
|
+
/**
|
|
19
|
+
* Merge AskUserQuestion into a session's disallowedTools when suppression is on.
|
|
20
|
+
* The Telegram answer round-trip silently drops taps, so a question there stalls
|
|
21
|
+
* the turn — suppressing the tool is the hard guarantee behind AUTONOMY_RULE's
|
|
22
|
+
* "decide, do not ask" posture. Returns `prior` unchanged when suppression is
|
|
23
|
+
* off (so callers only set disallowedTools when there's something to set). Pure
|
|
24
|
+
* + dedup so it's unit-testable and idempotent across repeated session creates.
|
|
25
|
+
*/
|
|
26
|
+
export declare function withAskUserSuppressed(prior: string[] | undefined, suppress: boolean): string[] | undefined;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTONOMY_RULE — "act, don't ask" posture injected into the Savvy chat path.
|
|
3
|
+
*
|
|
4
|
+
* Why: the openai-compat (Telegram) path does not reliably load the owner's
|
|
5
|
+
* CLAUDE.md "Execute, don't discuss" rules (tmpdir CWD), so the model reverts to
|
|
6
|
+
* Claude's base posture of asking on ambiguous forks. Combined with a Telegram
|
|
7
|
+
* AskUserQuestion round-trip that silently drops answers, this made Savvy stall
|
|
8
|
+
* and require many prods to finish one task. This rule restores the terminal-CLI
|
|
9
|
+
* posture: decide and execute, complete the whole task in one turn.
|
|
10
|
+
*
|
|
11
|
+
* Gated by CC_OPENCLAW_AUTONOMY_RULE (default on; set '0' to disable). Prepended
|
|
12
|
+
* at the same injection points as TTS_RULE in openai-compat.ts so it lands in
|
|
13
|
+
* both the REPLACE (--system-prompt) and APPEND (--append-system-prompt) paths.
|
|
14
|
+
*
|
|
15
|
+
* Pure-string constant — no I/O, no module state.
|
|
16
|
+
*/
|
|
17
|
+
export const AUTONOMY_RULE = [
|
|
18
|
+
'=== AUTONOMY RULE (cc-openclaw, takes precedence) ===',
|
|
19
|
+
'',
|
|
20
|
+
'You are operating over a chat channel for the owner, who cannot watch you',
|
|
21
|
+
'work and only sees your final reply. Default to ACTING, not asking.',
|
|
22
|
+
'',
|
|
23
|
+
'1. DECIDE, do not ask. For any reversible decision (layout, naming, which of',
|
|
24
|
+
' several valid approaches), pick the best option, state your reasoning in',
|
|
25
|
+
' ONE line, and proceed. Do NOT stop to ask "shall I proceed?", "A or B?",',
|
|
26
|
+
' or "want me to continue?" for routine work. Only pause for genuinely',
|
|
27
|
+
' destructive or irreversible actions (deleting data, force-push, sending',
|
|
28
|
+
' external messages).',
|
|
29
|
+
'',
|
|
30
|
+
'2. FINISH THE WHOLE TASK in one turn. When a request implies multiple steps',
|
|
31
|
+
' (e.g. fix -> build -> test -> commit -> ship), run ALL of them before',
|
|
32
|
+
' yielding. Do not hand back a half-done task with "say go to continue" —',
|
|
33
|
+
' that forces the owner to prod you repeatedly. Run it to completion.',
|
|
34
|
+
'',
|
|
35
|
+
'3. The ONE exception is restarting the gateway you run inside: never bounce it',
|
|
36
|
+
' mid-reply (it kills this message). Do every other step first, then schedule',
|
|
37
|
+
' the restart as a detached, delayed command so it lands AFTER your reply.',
|
|
38
|
+
'',
|
|
39
|
+
'4. Verify before claiming done. Run the build/test/probe and report what you',
|
|
40
|
+
' actually observed ("ran X, it returned Y"), never "should work".',
|
|
41
|
+
'',
|
|
42
|
+
'=== END AUTONOMY RULE ===',
|
|
43
|
+
].join('\n');
|
|
44
|
+
/**
|
|
45
|
+
* Merge AskUserQuestion into a session's disallowedTools when suppression is on.
|
|
46
|
+
* The Telegram answer round-trip silently drops taps, so a question there stalls
|
|
47
|
+
* the turn — suppressing the tool is the hard guarantee behind AUTONOMY_RULE's
|
|
48
|
+
* "decide, do not ask" posture. Returns `prior` unchanged when suppression is
|
|
49
|
+
* off (so callers only set disallowedTools when there's something to set). Pure
|
|
50
|
+
* + dedup so it's unit-testable and idempotent across repeated session creates.
|
|
51
|
+
*/
|
|
52
|
+
export function withAskUserSuppressed(prior, suppress) {
|
|
53
|
+
if (!suppress)
|
|
54
|
+
return prior;
|
|
55
|
+
return [...new Set([...(prior ?? []), 'AskUserQuestion'])];
|
|
56
|
+
}
|
|
@@ -29,6 +29,7 @@ import { getTtsAutoMode } from '../lib/config.js';
|
|
|
29
29
|
// suggestion. v0.10.3 explicitly forbids alternatives and gives an example.
|
|
30
30
|
// `TTS_RULE` extracted to `./tts-rule.ts` 2026-05-13 — pure-string constant.
|
|
31
31
|
import { TTS_RULE } from './tts-rule.js';
|
|
32
|
+
import { AUTONOMY_RULE, withAskUserSuppressed } from './autonomy-rule.js';
|
|
32
33
|
import { extractUserMessage, } from './message-extractor.js';
|
|
33
34
|
import { handleNonStreaming } from './non-streaming-handler.js';
|
|
34
35
|
import { handleStreaming } from './streaming-handler.js';
|
|
@@ -332,6 +333,16 @@ export async function handleChatCompletion(manager, body, headers, res) {
|
|
|
332
333
|
sessionConfig.tools = '';
|
|
333
334
|
}
|
|
334
335
|
}
|
|
336
|
+
// v0.27.11: suppress AskUserQuestion on the chat path (CC_OPENCLAW_SUPPRESS_ASKUSER,
|
|
337
|
+
// default on). The Telegram answer round-trip silently drops taps (askuser.ts
|
|
338
|
+
// injectAnswer → "SKIPPED sessionKey=none"), so a question there stalls the turn.
|
|
339
|
+
// This is the hard guarantee behind the AUTONOMY_RULE posture: the model decides
|
|
340
|
+
// instead of asking. Reversible via the env flag.
|
|
341
|
+
if (engine === 'claude') {
|
|
342
|
+
const suppressed = withAskUserSuppressed(sessionConfig.disallowedTools, process.env.CC_OPENCLAW_SUPPRESS_ASKUSER !== '0');
|
|
343
|
+
if (suppressed)
|
|
344
|
+
sessionConfig.disallowedTools = suppressed;
|
|
345
|
+
}
|
|
335
346
|
// Claude Code CLI supports --system-prompt (replace) and --append-system-prompt (append).
|
|
336
347
|
// When the caller provides tools, use --system-prompt to REPLACE the CLI's entire
|
|
337
348
|
// system prompt via buildSessionSystemPrompt(). See that function's doc for details
|
|
@@ -343,16 +354,21 @@ export async function handleChatCompletion(manager, body, headers, res) {
|
|
|
343
354
|
// [[tts:text]] syntax regardless of which CLI flag is used.
|
|
344
355
|
const ttsAuto = getTtsAutoMode();
|
|
345
356
|
const ttsPrefix = ttsAuto !== 'off' ? `${TTS_RULE}\n\n` : '';
|
|
357
|
+
// v0.27.11: "act, don't ask" posture (CC_OPENCLAW_AUTONOMY_RULE, default on).
|
|
358
|
+
// Restores the terminal-CLI execution posture the tmpdir-CWD chat path
|
|
359
|
+
// otherwise loses, so Savvy finishes the whole task instead of stalling.
|
|
360
|
+
const autonomyPrefix = process.env.CC_OPENCLAW_AUTONOMY_RULE !== '0' ? `${AUTONOMY_RULE}\n\n` : '';
|
|
361
|
+
const prefix = autonomyPrefix + ttsPrefix;
|
|
346
362
|
if (request.tools?.length) {
|
|
347
363
|
sessionConfig.systemPrompt =
|
|
348
|
-
|
|
364
|
+
prefix + buildSessionSystemPrompt(request.tools, extracted.systemPrompt);
|
|
349
365
|
}
|
|
350
366
|
else if (extracted.systemPrompt) {
|
|
351
|
-
sessionConfig.appendSystemPrompt =
|
|
367
|
+
sessionConfig.appendSystemPrompt = prefix + extracted.systemPrompt;
|
|
352
368
|
}
|
|
353
|
-
else if (
|
|
354
|
-
// No upstream system prompt but
|
|
355
|
-
sessionConfig.appendSystemPrompt =
|
|
369
|
+
else if (prefix) {
|
|
370
|
+
// No upstream system prompt but a rule prefix is on → inject just the rules
|
|
371
|
+
sessionConfig.appendSystemPrompt = prefix.trim();
|
|
356
372
|
}
|
|
357
373
|
}
|
|
358
374
|
try {
|
|
@@ -30,6 +30,21 @@ export interface PersistedSession {
|
|
|
30
30
|
* the decision is unit-testable independent of the disk layer.
|
|
31
31
|
*/
|
|
32
32
|
export declare function isPersistedSessionFresh(persisted: Pick<PersistedSession, 'lastActivity'> | undefined, now: number, freshnessMs: number): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* v0.27.11 — write-through decision for the resume id. The debounced disk save
|
|
35
|
+
* loses an unflushed claudeSessionId on a hard kill (cc-install / watchdog
|
|
36
|
+
* SIGTERM / detached pm2 restart) — exactly when post-restart resume matters.
|
|
37
|
+
* So a freshness-resume session flushes its id to disk synchronously the first
|
|
38
|
+
* time that id is seen. Returns true → caller does a synchronous save and
|
|
39
|
+
* records the id as flushed; false → caller uses the debounced save (fine for
|
|
40
|
+
* frequent same-id lastActivity bumps). Pure + side-effect-free for unit-testing.
|
|
41
|
+
*/
|
|
42
|
+
export declare function shouldWriteThroughResumeId(opts: {
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
optedFreshResume: boolean;
|
|
45
|
+
lastFlushedId: string | undefined;
|
|
46
|
+
newId: string | undefined;
|
|
47
|
+
}): boolean;
|
|
33
48
|
export declare function loadPersistedSessions(): Map<string, PersistedSession>;
|
|
34
49
|
export declare function savePersistedSessions(sessions: Map<string, PersistedSession>, logger?: Logger): void;
|
|
35
50
|
export declare function savePersistedSessionsAsync(sessions: Map<string, PersistedSession>, logger?: Logger): void;
|
|
@@ -31,6 +31,22 @@ export function isPersistedSessionFresh(persisted, now, freshnessMs) {
|
|
|
31
31
|
return false;
|
|
32
32
|
return now - persisted.lastActivity <= freshnessMs;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* v0.27.11 — write-through decision for the resume id. The debounced disk save
|
|
36
|
+
* loses an unflushed claudeSessionId on a hard kill (cc-install / watchdog
|
|
37
|
+
* SIGTERM / detached pm2 restart) — exactly when post-restart resume matters.
|
|
38
|
+
* So a freshness-resume session flushes its id to disk synchronously the first
|
|
39
|
+
* time that id is seen. Returns true → caller does a synchronous save and
|
|
40
|
+
* records the id as flushed; false → caller uses the debounced save (fine for
|
|
41
|
+
* frequent same-id lastActivity bumps). Pure + side-effect-free for unit-testing.
|
|
42
|
+
*/
|
|
43
|
+
export function shouldWriteThroughResumeId(opts) {
|
|
44
|
+
if (!opts.enabled || !opts.optedFreshResume)
|
|
45
|
+
return false;
|
|
46
|
+
if (!opts.newId)
|
|
47
|
+
return false;
|
|
48
|
+
return opts.lastFlushedId !== opts.newId;
|
|
49
|
+
}
|
|
34
50
|
export function loadPersistedSessions() {
|
|
35
51
|
try {
|
|
36
52
|
if (!fs.existsSync(PERSIST_FILE))
|
|
@@ -33,7 +33,7 @@ function getPluginVersion() {
|
|
|
33
33
|
// ─── Persistence ─────────────────────────────────────────────────────────────
|
|
34
34
|
// Extracted to `./persisted-sessions.ts` 2026-05-13 — coherent persistence
|
|
35
35
|
// layer (load + sync atomic-write + async-write + types + constants).
|
|
36
|
-
import { loadPersistedSessions, savePersistedSessions, savePersistedSessionsAsync, isPersistedSessionFresh, } from './persisted-sessions.js';
|
|
36
|
+
import { loadPersistedSessions, savePersistedSessions, savePersistedSessionsAsync, isPersistedSessionFresh, shouldWriteThroughResumeId, } from './persisted-sessions.js';
|
|
37
37
|
// Debounce helper — coalesces rapid writes into one
|
|
38
38
|
// `makeDebounced` extracted to `../lib/debounce.ts` 2026-05-13 —
|
|
39
39
|
// pure-function hot-path decomposition.
|
|
@@ -72,6 +72,10 @@ export class SessionManager {
|
|
|
72
72
|
pluginConfig;
|
|
73
73
|
persistedSessions;
|
|
74
74
|
_debouncedSave;
|
|
75
|
+
// v0.27.11: tracks the claudeSessionId last DURABLY (synchronously) written to
|
|
76
|
+
// disk per session name, so write-through fires once per new id rather than on
|
|
77
|
+
// every send. See _persistSession.
|
|
78
|
+
_lastFlushedIds = new Map();
|
|
75
79
|
_proxyServer = null;
|
|
76
80
|
_proxyPort = null;
|
|
77
81
|
_activePids = new Map();
|
|
@@ -429,6 +433,7 @@ export class SessionManager {
|
|
|
429
433
|
this._savePids();
|
|
430
434
|
// Explicit stop = user intent to end session — remove from disk too
|
|
431
435
|
this.persistedSessions.delete(name);
|
|
436
|
+
this._lastFlushedIds.delete(name);
|
|
432
437
|
savePersistedSessions(this.persistedSessions, this.logger);
|
|
433
438
|
}
|
|
434
439
|
listSessions() {
|
|
@@ -943,7 +948,26 @@ export class SessionManager {
|
|
|
943
948
|
lastResumed: new Date().toISOString(),
|
|
944
949
|
lastActivity: managed.lastActivity,
|
|
945
950
|
});
|
|
946
|
-
|
|
951
|
+
// v0.27.11: write-through the resume id. The debounced save (used for
|
|
952
|
+
// frequent lastActivity bumps) loses the claudeSessionId when the gateway is
|
|
953
|
+
// hard-killed before the timer fires — and hard restarts are common
|
|
954
|
+
// (cc-install, watchdog SIGTERM, detached pm2 restart), which is exactly when
|
|
955
|
+
// resume matters most. So when a freshness-resume session sees a NEW
|
|
956
|
+
// claudeSessionId, flush it to disk synchronously, once, immediately. Frequent
|
|
957
|
+
// same-id bumps stay debounced. Gated by CC_OPENCLAW_RESUME_WRITETHROUGH.
|
|
958
|
+
const optedFreshResume = typeof managed.config.resumeFreshnessMs === 'number' && managed.config.resumeFreshnessMs > 0;
|
|
959
|
+
if (shouldWriteThroughResumeId({
|
|
960
|
+
enabled: process.env.CC_OPENCLAW_RESUME_WRITETHROUGH !== '0',
|
|
961
|
+
optedFreshResume,
|
|
962
|
+
lastFlushedId: this._lastFlushedIds.get(name),
|
|
963
|
+
newId: managed.claudeSessionId,
|
|
964
|
+
})) {
|
|
965
|
+
savePersistedSessions(this.persistedSessions, this.logger);
|
|
966
|
+
this._lastFlushedIds.set(name, managed.claudeSessionId);
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
this._debouncedSave();
|
|
970
|
+
}
|
|
947
971
|
}
|
|
948
972
|
// ─── PID Tracking ──────────────────────────────────────────────────────
|
|
949
973
|
static PID_FILE = path.join(os.homedir(), '.openclaw', 'session-pids.json');
|
|
@@ -1523,6 +1547,7 @@ export class SessionManager {
|
|
|
1523
1547
|
for (const [name, entry] of this.persistedSessions) {
|
|
1524
1548
|
if (now - entry.lastActivity > PERSIST_DISK_TTL_MS) {
|
|
1525
1549
|
this.persistedSessions.delete(name);
|
|
1550
|
+
this._lastFlushedIds.delete(name);
|
|
1526
1551
|
pruned = true;
|
|
1527
1552
|
}
|
|
1528
1553
|
}
|