@a1hvdy/cc-openclaw 0.27.10 → 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/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
|
@@ -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
|
}
|