@a1hvdy/cc-openclaw 0.27.10 → 0.27.12

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.
@@ -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
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * resolveChatCwd — working directory for an openai-compat (Telegram chat) session.
3
+ *
4
+ * Default: a neutral per-session tmpdir, so the claude CLI does NOT load
5
+ * CLAUDE.md / git state / project context (keeps chat turns cheap and clean —
6
+ * this is the deliberate v0.x choice).
7
+ *
8
+ * Opt-in (v0.27.12): set CC_OPENCLAW_CHAT_CWD to an existing directory and chat
9
+ * sessions run there — the CLI then loads that project's CLAUDE.md + git context
10
+ * like the terminal CLI does, and the CLI's "cwd reset" lands on the project dir
11
+ * instead of a tmpdir. This closes the chat-vs-terminal project-context parity
12
+ * gap as an explicit, per-deployment opt-in, so the default stays zero-token-
13
+ * overhead. A blank/unset/nonexistent value falls back to the tmpdir, so a
14
+ * misconfigured path can never break session creation.
15
+ *
16
+ * Pure (deps injectable) for unit-testing.
17
+ */
18
+ export declare function resolveChatCwd(sessionName: string, opts?: {
19
+ cwdOverride?: string;
20
+ dirExists?: (p: string) => boolean;
21
+ tmpDir?: string;
22
+ }): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * resolveChatCwd — working directory for an openai-compat (Telegram chat) session.
3
+ *
4
+ * Default: a neutral per-session tmpdir, so the claude CLI does NOT load
5
+ * CLAUDE.md / git state / project context (keeps chat turns cheap and clean —
6
+ * this is the deliberate v0.x choice).
7
+ *
8
+ * Opt-in (v0.27.12): set CC_OPENCLAW_CHAT_CWD to an existing directory and chat
9
+ * sessions run there — the CLI then loads that project's CLAUDE.md + git context
10
+ * like the terminal CLI does, and the CLI's "cwd reset" lands on the project dir
11
+ * instead of a tmpdir. This closes the chat-vs-terminal project-context parity
12
+ * gap as an explicit, per-deployment opt-in, so the default stays zero-token-
13
+ * overhead. A blank/unset/nonexistent value falls back to the tmpdir, so a
14
+ * misconfigured path can never break session creation.
15
+ *
16
+ * Pure (deps injectable) for unit-testing.
17
+ */
18
+ import * as os from 'node:os';
19
+ import * as path from 'node:path';
20
+ import * as fs from 'node:fs';
21
+ export function resolveChatCwd(sessionName, opts = {}) {
22
+ const override = (opts.cwdOverride ?? process.env.CC_OPENCLAW_CHAT_CWD ?? '').trim();
23
+ const dirExists = opts.dirExists ??
24
+ ((p) => {
25
+ try {
26
+ return fs.statSync(p).isDirectory();
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ });
32
+ if (override && dirExists(override))
33
+ return override;
34
+ const base = opts.tmpDir ?? os.tmpdir();
35
+ return path.join(base, `openclaw-compat-${sessionName}`);
36
+ }
@@ -7,7 +7,6 @@
7
7
  */
8
8
  import * as fs from 'node:fs';
9
9
  import * as path from 'node:path';
10
- import * as os from 'node:os';
11
10
  import { randomUUID } from 'node:crypto';
12
11
  import { resolveEngineAndModel } from '../models.js';
13
12
  import { OPENAI_COMPAT_DEFAULT_MODEL, OPENAI_COMPAT_AUTO_COMPACT_THRESHOLD, RESUME_FRESHNESS_MS, } from '../constants.js';
@@ -29,6 +28,8 @@ import { getTtsAutoMode } from '../lib/config.js';
29
28
  // suggestion. v0.10.3 explicitly forbids alternatives and gives an example.
30
29
  // `TTS_RULE` extracted to `./tts-rule.ts` 2026-05-13 — pure-string constant.
31
30
  import { TTS_RULE } from './tts-rule.js';
31
+ import { AUTONOMY_RULE, withAskUserSuppressed } from './autonomy-rule.js';
32
+ import { resolveChatCwd } from './chat-cwd.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';
@@ -257,10 +258,12 @@ export async function handleChatCompletion(manager, body, headers, res) {
257
258
  // Create session if needed
258
259
  const needsCreate = !sessionExists || extracted.isNewConversation;
259
260
  if (needsCreate) {
260
- // OpenAI-compat sessions are API proxies, not coding sessions.
261
- // Use a neutral empty temp dir so the CLI doesn't load CLAUDE.md,
262
- // git state, or project context from wherever `serve` was started.
263
- const sessionCwd = path.join(os.tmpdir(), `openclaw-compat-${sessionName}`);
261
+ // OpenAI-compat sessions are API proxies, not coding sessions. By default
262
+ // use a neutral empty temp dir so the CLI doesn't load CLAUDE.md, git state,
263
+ // or project context from wherever `serve` was started. v0.27.12: opt into a
264
+ // real project dir via CC_OPENCLAW_CHAT_CWD to load project context like the
265
+ // terminal CLI (see resolveChatCwd). Default unchanged = zero token overhead.
266
+ const sessionCwd = resolveChatCwd(sessionName);
264
267
  if (!fs.existsSync(sessionCwd))
265
268
  fs.mkdirSync(sessionCwd, { recursive: true });
266
269
  const sessionConfig = {
@@ -332,6 +335,16 @@ export async function handleChatCompletion(manager, body, headers, res) {
332
335
  sessionConfig.tools = '';
333
336
  }
334
337
  }
338
+ // v0.27.11: suppress AskUserQuestion on the chat path (CC_OPENCLAW_SUPPRESS_ASKUSER,
339
+ // default on). The Telegram answer round-trip silently drops taps (askuser.ts
340
+ // injectAnswer → "SKIPPED sessionKey=none"), so a question there stalls the turn.
341
+ // This is the hard guarantee behind the AUTONOMY_RULE posture: the model decides
342
+ // instead of asking. Reversible via the env flag.
343
+ if (engine === 'claude') {
344
+ const suppressed = withAskUserSuppressed(sessionConfig.disallowedTools, process.env.CC_OPENCLAW_SUPPRESS_ASKUSER !== '0');
345
+ if (suppressed)
346
+ sessionConfig.disallowedTools = suppressed;
347
+ }
335
348
  // Claude Code CLI supports --system-prompt (replace) and --append-system-prompt (append).
336
349
  // When the caller provides tools, use --system-prompt to REPLACE the CLI's entire
337
350
  // system prompt via buildSessionSystemPrompt(). See that function's doc for details
@@ -343,16 +356,21 @@ export async function handleChatCompletion(manager, body, headers, res) {
343
356
  // [[tts:text]] syntax regardless of which CLI flag is used.
344
357
  const ttsAuto = getTtsAutoMode();
345
358
  const ttsPrefix = ttsAuto !== 'off' ? `${TTS_RULE}\n\n` : '';
359
+ // v0.27.11: "act, don't ask" posture (CC_OPENCLAW_AUTONOMY_RULE, default on).
360
+ // Restores the terminal-CLI execution posture the tmpdir-CWD chat path
361
+ // otherwise loses, so Savvy finishes the whole task instead of stalling.
362
+ const autonomyPrefix = process.env.CC_OPENCLAW_AUTONOMY_RULE !== '0' ? `${AUTONOMY_RULE}\n\n` : '';
363
+ const prefix = autonomyPrefix + ttsPrefix;
346
364
  if (request.tools?.length) {
347
365
  sessionConfig.systemPrompt =
348
- ttsPrefix + buildSessionSystemPrompt(request.tools, extracted.systemPrompt);
366
+ prefix + buildSessionSystemPrompt(request.tools, extracted.systemPrompt);
349
367
  }
350
368
  else if (extracted.systemPrompt) {
351
- sessionConfig.appendSystemPrompt = ttsPrefix + extracted.systemPrompt;
369
+ sessionConfig.appendSystemPrompt = prefix + extracted.systemPrompt;
352
370
  }
353
- else if (ttsPrefix) {
354
- // No upstream system prompt but TTS is on → inject just the rule
355
- sessionConfig.appendSystemPrompt = ttsPrefix.trim();
371
+ else if (prefix) {
372
+ // No upstream system prompt but a rule prefix is on → inject just the rules
373
+ sessionConfig.appendSystemPrompt = prefix.trim();
356
374
  }
357
375
  }
358
376
  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))
@@ -24,6 +24,7 @@ export declare class SessionManager {
24
24
  private pluginConfig;
25
25
  private persistedSessions;
26
26
  private _debouncedSave;
27
+ private _lastFlushedIds;
27
28
  private _proxyServer;
28
29
  private _proxyPort;
29
30
  private _activePids;
@@ -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
- this._debouncedSave();
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a1hvdy/cc-openclaw",
3
- "version": "0.27.10",
3
+ "version": "0.27.12",
4
4
  "description": "A1xAI's Anthropic CLI bridge plugin for OpenClaw",
5
5
  "author": "@a1cy",
6
6
  "license": "MIT",