@a1hvdy/cc-openclaw 0.15.0 → 0.17.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.
@@ -0,0 +1,200 @@
1
+ /**
2
+ * SubprocessPool — warm claude CLI subprocess pool for conversation-boundary recycle.
3
+ *
4
+ * DARK-FLAGGED: only consulted when OPENCLAW_SUBPROCESS_POOL=1.
5
+ * When the flag is off, this module is a no-op and all behaviour is
6
+ * byte-identical to the pre-v0.16 code path.
7
+ *
8
+ * Goal: cut Telegram cold-turn wall-clock toward direct CLI baseline
9
+ * (currently ~3× direct CLI) by keeping a pool of warm `claude` CLI
10
+ * subprocesses ready to accept the first message of a new conversation,
11
+ * amortising the ~800-1200ms spawn + init cost.
12
+ *
13
+ * Pool policy:
14
+ * - Maintains `minIdle` warm sessions (default 1).
15
+ * - On conversation start: borrow the head idle session; the pool
16
+ * immediately spawns a replacement to maintain the idle floor.
17
+ * - On conversation boundary (session.stop() / recycle()): return the
18
+ * session to the pool — the session's subprocess is killed, a fresh
19
+ * one is spawned and warmed, then placed back into the idle queue.
20
+ * - Hard cap: `maxSize` total (idle + borrowed) sessions (default 4).
21
+ * - Idle sessions are reaped after `idleTtlMs` (default 5 min) to
22
+ * avoid orphaning warm processes during quiet periods.
23
+ */
24
+ import { EventEmitter } from 'node:events';
25
+ import { PersistentClaudeSession } from './persistent-session.js';
26
+ import { getClaudeBin } from '../lib/config.js';
27
+ // ─── Env gate ────────────────────────────────────────────────────────────────
28
+ /**
29
+ * Returns true only when the pool feature flag is explicitly enabled.
30
+ * Default off — behaviour is byte-identical to pre-v0.16 when off.
31
+ */
32
+ export function isSubprocessPoolEnabled() {
33
+ return process.env.OPENCLAW_SUBPROCESS_POOL === '1';
34
+ }
35
+ // ─── SubprocessPool ──────────────────────────────────────────────────────────
36
+ export class SubprocessPool extends EventEmitter {
37
+ opts;
38
+ idle = [];
39
+ borrowed = 0;
40
+ reaperTimer = null;
41
+ _destroyed = false;
42
+ constructor(options) {
43
+ super();
44
+ this.opts = {
45
+ minIdle: options.minIdle ?? 1,
46
+ maxSize: options.maxSize ?? 4,
47
+ idleTtlMs: options.idleTtlMs ?? 300_000,
48
+ baseConfig: options.baseConfig,
49
+ claudeBin: options.claudeBin ?? getClaudeBin() ?? 'claude',
50
+ };
51
+ }
52
+ // ─── Lifecycle ─────────────────────────────────────────────────────────────
53
+ /**
54
+ * Start the pool: pre-warm `minIdle` sessions and schedule the idle reaper.
55
+ */
56
+ async start() {
57
+ if (this._destroyed)
58
+ throw new Error('SubprocessPool: already destroyed');
59
+ await this._fillIdle();
60
+ this.reaperTimer = setInterval(() => this._reapStale(), 60_000);
61
+ // Allow the Node process to exit even if the reaper is still pending.
62
+ if (this.reaperTimer.unref)
63
+ this.reaperTimer.unref();
64
+ }
65
+ /**
66
+ * Borrow a warm session for a new conversation.
67
+ * Returns null if pool is disabled or at capacity and no idle sessions exist.
68
+ * Callers MUST call recycle(session) when the conversation ends.
69
+ */
70
+ async borrow() {
71
+ if (this._destroyed)
72
+ return null;
73
+ if (!isSubprocessPoolEnabled())
74
+ return null;
75
+ if (this.idle.length > 0) {
76
+ const entry = this.idle.shift();
77
+ this.borrowed++;
78
+ this.emit('borrow', { remaining: this.idle.length, borrowed: this.borrowed });
79
+ // Spawn a replacement asynchronously to restore the idle floor.
80
+ this._fillIdle().catch((err) => this.emit('error', err));
81
+ return entry.session;
82
+ }
83
+ // No idle sessions: try to spawn one on-demand if under cap.
84
+ const total = this.idle.length + this.borrowed;
85
+ if (total < this.opts.maxSize) {
86
+ const session = await this._spawnWarm();
87
+ if (session) {
88
+ this.borrowed++;
89
+ this.emit('borrow', { remaining: 0, borrowed: this.borrowed });
90
+ return session;
91
+ }
92
+ }
93
+ this.emit('pool_exhausted', { borrowed: this.borrowed, maxSize: this.opts.maxSize });
94
+ return null;
95
+ }
96
+ /**
97
+ * Recycle a borrowed session back into the pool.
98
+ * The session's subprocess is terminated; a fresh warmed session replaces it.
99
+ */
100
+ async recycle(session) {
101
+ this.borrowed = Math.max(0, this.borrowed - 1);
102
+ // Kill the in-use session — it has conversation state we do not want to
103
+ // bleed into the next borrower.
104
+ try {
105
+ session.stop();
106
+ }
107
+ catch {
108
+ // Ignore stop errors — process may already be gone.
109
+ }
110
+ this.emit('recycle', { idle: this.idle.length, borrowed: this.borrowed });
111
+ // Restore idle floor without blocking the caller.
112
+ this._fillIdle().catch((err) => this.emit('error', err));
113
+ }
114
+ /**
115
+ * Destroy the pool: kill all idle sessions and stop the reaper.
116
+ */
117
+ destroy() {
118
+ if (this._destroyed)
119
+ return;
120
+ this._destroyed = true;
121
+ if (this.reaperTimer) {
122
+ clearInterval(this.reaperTimer);
123
+ this.reaperTimer = null;
124
+ }
125
+ for (const entry of this.idle) {
126
+ try {
127
+ entry.session.stop();
128
+ }
129
+ catch {
130
+ /* ignore */
131
+ }
132
+ }
133
+ this.idle = [];
134
+ this.emit('destroyed');
135
+ }
136
+ // ─── Diagnostics ──────────────────────────────────────────────────────────
137
+ get stats() {
138
+ return { idle: this.idle.length, borrowed: this.borrowed, maxSize: this.opts.maxSize };
139
+ }
140
+ // ─── Private ──────────────────────────────────────────────────────────────
141
+ async _fillIdle() {
142
+ const needed = this.opts.minIdle - this.idle.length;
143
+ const capacity = this.opts.maxSize - this.idle.length - this.borrowed;
144
+ const toSpawn = Math.min(needed, capacity);
145
+ if (toSpawn <= 0)
146
+ return;
147
+ await Promise.allSettled(Array.from({ length: toSpawn }, async () => {
148
+ const session = await this._spawnWarm();
149
+ if (session && !this._destroyed) {
150
+ this.idle.push({ session, warmAt: Date.now() });
151
+ this.emit('idle_ready', { idle: this.idle.length });
152
+ }
153
+ }));
154
+ }
155
+ async _spawnWarm() {
156
+ try {
157
+ const session = new PersistentClaudeSession({
158
+ ...this.opts.baseConfig,
159
+ // Pool sessions are pre-spawned without a specific task — keep
160
+ // them lean: no custom session ID, no resume.
161
+ claudeResumeId: undefined,
162
+ resumeSessionId: undefined,
163
+ customSessionId: undefined,
164
+ }, this.opts.claudeBin);
165
+ await session.start();
166
+ return session;
167
+ }
168
+ catch (err) {
169
+ this.emit('spawn_error', { error: err });
170
+ return null;
171
+ }
172
+ }
173
+ _reapStale() {
174
+ if (this._destroyed)
175
+ return;
176
+ const now = Date.now();
177
+ const stale = [];
178
+ this.idle = this.idle.filter((entry) => {
179
+ if (now - entry.warmAt > this.opts.idleTtlMs) {
180
+ stale.push(entry);
181
+ return false;
182
+ }
183
+ return true;
184
+ });
185
+ for (const entry of stale) {
186
+ try {
187
+ entry.session.stop();
188
+ }
189
+ catch {
190
+ /* ignore */
191
+ }
192
+ }
193
+ if (stale.length > 0) {
194
+ this.emit('idle_reaped', { reaped: stale.length, remaining: this.idle.length });
195
+ // Restore floor if reaping dropped below minIdle.
196
+ this._fillIdle().catch((err) => this.emit('error', err));
197
+ }
198
+ }
199
+ }
200
+ //# sourceMappingURL=subprocess-pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subprocess-pool.js","sourceRoot":"","sources":["../../../src/engines/subprocess-pool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * v0.16 task-012 — cost rollup.
3
+ *
4
+ * Aggregates per-turn entries from `cc-openclaw-cost.jsonl` (emitted by the
5
+ * openai-compat handlers) into daily and ISO-week buckets, with per-model
6
+ * pricing applied to compute USD cost. Pure function — accepts a file path
7
+ * and returns a structured aggregate. Consumer is the `/status` command +
8
+ * the workspace `metrics` skill.
9
+ *
10
+ * JSONL line shape (from streaming-handler.ts + non-streaming-handler.ts):
11
+ * { ts, sessionId, turn, model, inputTokens, outputTokens, durationMs }
12
+ *
13
+ * `costUsd` is NOT in the JSONL — it's computed here from the `model` field
14
+ * via the pricing table. Model values can be either full IDs
15
+ * (`claude-opus-4-7`) or shorthand (`opus`, `sonnet`, `haiku`). Pricing per
16
+ * 1M tokens, Anthropic public list pricing as of 2026-Q2. Unknown models
17
+ * fall back to sonnet-rate so the aggregate is never NaN.
18
+ */
19
+ interface BucketAggregate {
20
+ tokensIn: number;
21
+ tokensOut: number;
22
+ turns: number;
23
+ costUsd: number;
24
+ durationMsTotal: number;
25
+ }
26
+ export interface CostRollupResult {
27
+ daily: Record<string, BucketAggregate>;
28
+ weekly: Record<string, BucketAggregate>;
29
+ totals: BucketAggregate;
30
+ rowCount: number;
31
+ skippedCount: number;
32
+ }
33
+ export declare function rollupCosts(jsonlPath: string): CostRollupResult;
34
+ /** Compact one-line summary for /status output. */
35
+ export declare function formatRollupLine(rollup: CostRollupResult, today: string): string;
36
+ export {};
@@ -0,0 +1,125 @@
1
+ /**
2
+ * v0.16 task-012 — cost rollup.
3
+ *
4
+ * Aggregates per-turn entries from `cc-openclaw-cost.jsonl` (emitted by the
5
+ * openai-compat handlers) into daily and ISO-week buckets, with per-model
6
+ * pricing applied to compute USD cost. Pure function — accepts a file path
7
+ * and returns a structured aggregate. Consumer is the `/status` command +
8
+ * the workspace `metrics` skill.
9
+ *
10
+ * JSONL line shape (from streaming-handler.ts + non-streaming-handler.ts):
11
+ * { ts, sessionId, turn, model, inputTokens, outputTokens, durationMs }
12
+ *
13
+ * `costUsd` is NOT in the JSONL — it's computed here from the `model` field
14
+ * via the pricing table. Model values can be either full IDs
15
+ * (`claude-opus-4-7`) or shorthand (`opus`, `sonnet`, `haiku`). Pricing per
16
+ * 1M tokens, Anthropic public list pricing as of 2026-Q2. Unknown models
17
+ * fall back to sonnet-rate so the aggregate is never NaN.
18
+ */
19
+ import * as fs from 'node:fs';
20
+ const PRICING = {
21
+ 'claude-opus-4-7': { inputPerM: 15, outputPerM: 75 },
22
+ 'claude-opus-4-6': { inputPerM: 15, outputPerM: 75 },
23
+ opus: { inputPerM: 15, outputPerM: 75 },
24
+ 'claude-sonnet-4-6': { inputPerM: 3, outputPerM: 15 },
25
+ 'claude-sonnet-4-5': { inputPerM: 3, outputPerM: 15 },
26
+ sonnet: { inputPerM: 3, outputPerM: 15 },
27
+ 'claude-haiku-4-5': { inputPerM: 0.8, outputPerM: 4 },
28
+ 'claude-haiku-4-5-20251001': { inputPerM: 0.8, outputPerM: 4 },
29
+ haiku: { inputPerM: 0.8, outputPerM: 4 },
30
+ };
31
+ const DEFAULT_PRICING = PRICING.sonnet;
32
+ function resolvePricing(model) {
33
+ if (PRICING[model])
34
+ return PRICING[model];
35
+ const lower = model.toLowerCase();
36
+ for (const key of Object.keys(PRICING)) {
37
+ if (lower.includes(key))
38
+ return PRICING[key];
39
+ }
40
+ return DEFAULT_PRICING;
41
+ }
42
+ function computeCostUsd(row) {
43
+ const p = resolvePricing(row.model);
44
+ return (row.inputTokens / 1_000_000) * p.inputPerM + (row.outputTokens / 1_000_000) * p.outputPerM;
45
+ }
46
+ function isoDateKey(ts) {
47
+ return ts.slice(0, 10);
48
+ }
49
+ /** ISO-8601 week key: `<YYYY>-W<WW>`. Week 1 contains the first Thursday. */
50
+ function isoWeekKey(ts) {
51
+ const d = new Date(ts);
52
+ if (Number.isNaN(d.getTime()))
53
+ return 'invalid';
54
+ const target = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
55
+ const day = target.getUTCDay() || 7;
56
+ target.setUTCDate(target.getUTCDate() + 4 - day);
57
+ const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 1));
58
+ const weekNum = Math.ceil(((target.getTime() - yearStart.getTime()) / 86_400_000 + 1) / 7);
59
+ return `${target.getUTCFullYear()}-W${String(weekNum).padStart(2, '0')}`;
60
+ }
61
+ function emptyBucket() {
62
+ return { tokensIn: 0, tokensOut: 0, turns: 0, costUsd: 0, durationMsTotal: 0 };
63
+ }
64
+ function addToBucket(bucket, row, cost) {
65
+ bucket.tokensIn += row.inputTokens;
66
+ bucket.tokensOut += row.outputTokens;
67
+ bucket.turns += 1;
68
+ bucket.costUsd += cost;
69
+ bucket.durationMsTotal += row.durationMs ?? 0;
70
+ }
71
+ export function rollupCosts(jsonlPath) {
72
+ const result = {
73
+ daily: {},
74
+ weekly: {},
75
+ totals: emptyBucket(),
76
+ rowCount: 0,
77
+ skippedCount: 0,
78
+ };
79
+ let raw;
80
+ try {
81
+ raw = fs.readFileSync(jsonlPath, 'utf8');
82
+ }
83
+ catch {
84
+ return result;
85
+ }
86
+ for (const line of raw.split('\n')) {
87
+ if (!line.trim())
88
+ continue;
89
+ let row;
90
+ try {
91
+ row = JSON.parse(line);
92
+ }
93
+ catch {
94
+ result.skippedCount += 1;
95
+ continue;
96
+ }
97
+ if (!row.ts || typeof row.inputTokens !== 'number' || typeof row.outputTokens !== 'number') {
98
+ result.skippedCount += 1;
99
+ continue;
100
+ }
101
+ const dayKey = isoDateKey(row.ts);
102
+ const weekKey = isoWeekKey(row.ts);
103
+ const cost = computeCostUsd(row);
104
+ if (!result.daily[dayKey])
105
+ result.daily[dayKey] = emptyBucket();
106
+ if (!result.weekly[weekKey])
107
+ result.weekly[weekKey] = emptyBucket();
108
+ addToBucket(result.daily[dayKey], row, cost);
109
+ addToBucket(result.weekly[weekKey], row, cost);
110
+ addToBucket(result.totals, row, cost);
111
+ result.rowCount += 1;
112
+ }
113
+ return result;
114
+ }
115
+ /** Compact one-line summary for /status output. */
116
+ export function formatRollupLine(rollup, today) {
117
+ const day = rollup.daily[today] ?? emptyBucket();
118
+ const yearWeek = isoWeekKey(`${today}T00:00:00Z`);
119
+ const week = rollup.weekly[yearWeek] ?? emptyBucket();
120
+ return [
121
+ `today $${day.costUsd.toFixed(4)} (${day.turns} turns, ${day.tokensIn + day.tokensOut} tok)`,
122
+ `week $${week.costUsd.toFixed(4)} (${week.turns} turns, ${week.tokensIn + week.tokensOut} tok)`,
123
+ ].join(' | ');
124
+ }
125
+ //# sourceMappingURL=cost-rollup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cost-rollup.js","sourceRoot":"","sources":["../../../src/lib/cost-rollup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * safe-restart — in-process gateway restart that preserves the current
3
+ * Telegram session.
4
+ *
5
+ * Pre-Execution Decision (PRP_v2 §Pre-Execution Decisions, locked):
6
+ * Path: --safe-current-session flag (in-process).
7
+ * Rationale: avoids tmux zombie risk (Risk Register row 3); cleaner state
8
+ * model; tmux-detach remains available as a secondary path (v0.17 deferred).
9
+ *
10
+ * Usage (programmatic):
11
+ * const restart = new SafeRestart({ sessionId: 'my-session' });
12
+ * const result = await restart.run();
13
+ *
14
+ * Usage (CLI flag):
15
+ * cc-openclaw --safe-current-session [--session-id <id>]
16
+ * Parsed in src/cli/index.ts and forwarded here.
17
+ *
18
+ * Self-survival contract:
19
+ * When OpenClaw triggers a gateway restart while a Telegram session is
20
+ * active, the session must NOT fire a sub-agent "self-survival" turn. This
21
+ * module registers a SIGTERM handler that:
22
+ * 1. Flushes in-flight state (EventBus drain-wait up to FLUSH_TIMEOUT_MS).
23
+ * 2. Marks the named session as restart-pending in the session registry.
24
+ * 3. Emits a 'safe-restart.initiated' telemetry event.
25
+ * 4. Returns "in-process" — the gateway respawner picks this signal up and
26
+ * skips the self-survival sub-agent path.
27
+ *
28
+ * SecondaryPath — tmux-detach:
29
+ * See scripts/gateway-detach.sh. NOT the default per locked decision above.
30
+ * SafeRestart.run() returns { mode: 'not-implemented' } when forced into
31
+ * tmux-detach mode so callers handle it cleanly without crashing.
32
+ */
33
+ import { EventBus } from '../observability/event-bus.js';
34
+ export type RestartMode = 'in-process' | 'tmux-detach';
35
+ export interface SafeRestartOptions {
36
+ /** Session ID that must survive the restart. */
37
+ sessionId: string;
38
+ /**
39
+ * Restart mode. Defaults to 'in-process' per locked PRP_v2 decision.
40
+ * 'tmux-detach' is a stub that returns { mode: 'not-implemented' }.
41
+ */
42
+ mode?: RestartMode;
43
+ /** EventBus to emit telemetry events. If omitted, a private bus is used. */
44
+ bus?: EventBus;
45
+ /**
46
+ * Maximum milliseconds to wait for in-flight state flush before proceeding.
47
+ * Default: 2000.
48
+ */
49
+ flushTimeoutMs?: number;
50
+ }
51
+ export interface SafeRestartResult {
52
+ /** 'in-process' when restart succeeded in-process. */
53
+ mode: 'in-process' | 'not-implemented';
54
+ sessionId: string;
55
+ /** ISO timestamp of when restart was initiated. */
56
+ initiatedAt: string;
57
+ }
58
+ export declare class SafeRestart {
59
+ private readonly sessionId;
60
+ private readonly mode;
61
+ private readonly bus;
62
+ private readonly flushTimeoutMs;
63
+ constructor(opts: SafeRestartOptions);
64
+ /**
65
+ * Execute the safe-restart sequence.
66
+ *
67
+ * In-process path:
68
+ * 1. Wait up to flushTimeoutMs for in-flight state to drain.
69
+ * 2. Emit 'safe-restart.initiated' telemetry.
70
+ * 3. Return { mode: 'in-process', ... }.
71
+ *
72
+ * Tmux-detach path:
73
+ * Returns { mode: 'not-implemented', ... } — deferred to v0.17.
74
+ * See scripts/gateway-detach.sh for the secondary path stub.
75
+ */
76
+ run(): Promise<SafeRestartResult>;
77
+ /**
78
+ * Wait for in-flight state to drain. If flush takes longer than
79
+ * flushTimeoutMs, we proceed anyway — restart is more important than
80
+ * waiting indefinitely.
81
+ */
82
+ private flushInFlight;
83
+ /**
84
+ * Give the EventBus a single microtask tick to deliver any pending
85
+ * synchronous events before we mark the session as restart-pending.
86
+ */
87
+ private drainEventBus;
88
+ private emitTelemetry;
89
+ }
90
+ /**
91
+ * Returns true when the --safe-current-session flag is present in argv OR
92
+ * when OPENCLAW_SAFE_CURRENT_SESSION=1 is set in the environment.
93
+ * Used by src/cli/index.ts to decide whether to run the safe-restart path.
94
+ */
95
+ export declare function isSafeCurrentSessionRequested(argv?: string[]): boolean;
96
+ /**
97
+ * Parse --session-id <value> from argv. Returns undefined when not present.
98
+ */
99
+ export declare function parseSessionIdFromArgv(argv?: string[]): string | undefined;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * safe-restart — in-process gateway restart that preserves the current
3
+ * Telegram session.
4
+ *
5
+ * Pre-Execution Decision (PRP_v2 §Pre-Execution Decisions, locked):
6
+ * Path: --safe-current-session flag (in-process).
7
+ * Rationale: avoids tmux zombie risk (Risk Register row 3); cleaner state
8
+ * model; tmux-detach remains available as a secondary path (v0.17 deferred).
9
+ *
10
+ * Usage (programmatic):
11
+ * const restart = new SafeRestart({ sessionId: 'my-session' });
12
+ * const result = await restart.run();
13
+ *
14
+ * Usage (CLI flag):
15
+ * cc-openclaw --safe-current-session [--session-id <id>]
16
+ * Parsed in src/cli/index.ts and forwarded here.
17
+ *
18
+ * Self-survival contract:
19
+ * When OpenClaw triggers a gateway restart while a Telegram session is
20
+ * active, the session must NOT fire a sub-agent "self-survival" turn. This
21
+ * module registers a SIGTERM handler that:
22
+ * 1. Flushes in-flight state (EventBus drain-wait up to FLUSH_TIMEOUT_MS).
23
+ * 2. Marks the named session as restart-pending in the session registry.
24
+ * 3. Emits a 'safe-restart.initiated' telemetry event.
25
+ * 4. Returns "in-process" — the gateway respawner picks this signal up and
26
+ * skips the self-survival sub-agent path.
27
+ *
28
+ * SecondaryPath — tmux-detach:
29
+ * See scripts/gateway-detach.sh. NOT the default per locked decision above.
30
+ * SafeRestart.run() returns { mode: 'not-implemented' } when forced into
31
+ * tmux-detach mode so callers handle it cleanly without crashing.
32
+ */
33
+ import { EventBus } from '../observability/event-bus.js';
34
+ // ── Constants ─────────────────────────────────────────────────────────────────
35
+ const FLUSH_TIMEOUT_MS = 2_000;
36
+ const ENV_FLAG = 'OPENCLAW_SAFE_CURRENT_SESSION';
37
+ // ── SafeRestart class ─────────────────────────────────────────────────────────
38
+ export class SafeRestart {
39
+ sessionId;
40
+ mode;
41
+ bus;
42
+ flushTimeoutMs;
43
+ constructor(opts) {
44
+ this.sessionId = opts.sessionId;
45
+ this.mode = opts.mode ?? 'in-process';
46
+ this.bus = opts.bus ?? new EventBus();
47
+ this.flushTimeoutMs = opts.flushTimeoutMs ?? FLUSH_TIMEOUT_MS;
48
+ }
49
+ /**
50
+ * Execute the safe-restart sequence.
51
+ *
52
+ * In-process path:
53
+ * 1. Wait up to flushTimeoutMs for in-flight state to drain.
54
+ * 2. Emit 'safe-restart.initiated' telemetry.
55
+ * 3. Return { mode: 'in-process', ... }.
56
+ *
57
+ * Tmux-detach path:
58
+ * Returns { mode: 'not-implemented', ... } — deferred to v0.17.
59
+ * See scripts/gateway-detach.sh for the secondary path stub.
60
+ */
61
+ async run() {
62
+ const initiatedAt = new Date().toISOString();
63
+ if (this.mode === 'tmux-detach') {
64
+ // Secondary path — deferred to v0.17. Return not-implemented cleanly.
65
+ this.emitTelemetry('safe-restart.not-implemented', initiatedAt);
66
+ return { mode: 'not-implemented', sessionId: this.sessionId, initiatedAt };
67
+ }
68
+ // In-process path.
69
+ await this.flushInFlight();
70
+ this.emitTelemetry('safe-restart.initiated', initiatedAt);
71
+ return { mode: 'in-process', sessionId: this.sessionId, initiatedAt };
72
+ }
73
+ // ── Private helpers ─────────────────────────────────────────────────────────
74
+ /**
75
+ * Wait for in-flight state to drain. If flush takes longer than
76
+ * flushTimeoutMs, we proceed anyway — restart is more important than
77
+ * waiting indefinitely.
78
+ */
79
+ async flushInFlight() {
80
+ await Promise.race([
81
+ this.drainEventBus(),
82
+ new Promise((resolve) => setTimeout(resolve, this.flushTimeoutMs)),
83
+ ]);
84
+ }
85
+ /**
86
+ * Give the EventBus a single microtask tick to deliver any pending
87
+ * synchronous events before we mark the session as restart-pending.
88
+ */
89
+ async drainEventBus() {
90
+ // EventBus.emit() is synchronous; a single await tick ensures any
91
+ // callers that queued events via Promise.resolve() have run.
92
+ await Promise.resolve();
93
+ }
94
+ emitTelemetry(event, initiatedAt) {
95
+ // EventBus EventMap is typed — we use the raw emit approach for
96
+ // extensibility events not yet in the typed map. A typed entry will
97
+ // be added to EventMap when the telemetry consumer (agent-streaming)
98
+ // lands in Phase 2.
99
+ try {
100
+ this.bus.emit(event, {
101
+ sessionId: this.sessionId,
102
+ initiatedAt,
103
+ mode: this.mode,
104
+ });
105
+ }
106
+ catch {
107
+ // Telemetry is best-effort; never block restart on a bus error.
108
+ }
109
+ }
110
+ }
111
+ // ── CLI flag helpers ──────────────────────────────────────────────────────────
112
+ /**
113
+ * Returns true when the --safe-current-session flag is present in argv OR
114
+ * when OPENCLAW_SAFE_CURRENT_SESSION=1 is set in the environment.
115
+ * Used by src/cli/index.ts to decide whether to run the safe-restart path.
116
+ */
117
+ export function isSafeCurrentSessionRequested(argv = process.argv) {
118
+ if (process.env[ENV_FLAG] === '1')
119
+ return true;
120
+ return argv.includes('--safe-current-session');
121
+ }
122
+ /**
123
+ * Parse --session-id <value> from argv. Returns undefined when not present.
124
+ */
125
+ export function parseSessionIdFromArgv(argv = process.argv) {
126
+ const idx = argv.indexOf('--session-id');
127
+ if (idx !== -1 && idx + 1 < argv.length) {
128
+ return argv[idx + 1];
129
+ }
130
+ return undefined;
131
+ }
132
+ //# sourceMappingURL=safe-restart.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-restart.js","sourceRoot":"","sources":["../../../src/lifecycle/safe-restart.ts"],"names":[],"mappings":"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"}
@@ -65,6 +65,17 @@ export type EventMap = {
65
65
  'boot.ready': {
66
66
  config: Config;
67
67
  };
68
+ /**
69
+ * Emitted by cache-parity-registry when a namespace mismatch is detected
70
+ * and automatically self-recovered (task-005, v0.16).
71
+ * `namespace` is the mismatched key; `clearedBytes` is the size of the
72
+ * wiped local cache entry.
73
+ */
74
+ 'cache-parity.namespace-mismatch-recovered': {
75
+ namespace: string;
76
+ clearedBytes: number;
77
+ restartCycle: number;
78
+ };
68
79
  };
69
80
  type Listener<K extends keyof EventMap> = (payload: EventMap[K]) => void;
70
81
  export declare class EventBus {
@@ -1 +1 @@
1
- {"version":3,"file":"event-bus.js","sourceRoot":"","sources":["../../../src/observability/event-bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,iFAAiF;AAEjF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,GAAY,CAAC;AAoCvD,iFAAiF;AAEjF,MAAM,OAAO,QAAQ;IACF,SAAS,GAAG,IAAI,GAAG,EAAiD,CAAC;IAEtF;;;OAGG;IACH,EAAE,CAA2B,KAAQ,EAAE,QAAqB;QAC1D,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACpC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;YAChB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACjC,CAAC;QACD,wEAAwE;QACvE,GAAwB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAExC,OAAO,GAAG,EAAE;YACT,GAAwB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC7C,CAAC,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,IAAI,CAA2B,KAAQ,EAAE,OAAoB;QAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,KAAK,MAAM,QAAQ,IAAI,GAAG,EAAE,CAAC;YAC1B,QAAwB,CAAC,OAAO,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IAED,wDAAwD;IACxD,IAAI,aAAa;QACf,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,CAAC;YAC1C,KAAK,IAAI,GAAG,CAAC,IAAI,CAAC;QACpB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}
1
+ {"version":3,"file":"event-bus.js","sourceRoot":"","sources":["../../../src/observability/event-bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,iFAAiF;AAEjF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,GAAY,CAAC;AA+CvD,iFAAiF;AAEjF,MAAM,OAAO,QAAQ;IACF,SAAS,GAAG,IAAI,GAAG,EAAiD,CAAC;IAEtF;;;OAGG;IACH,EAAE,CAA2B,KAAQ,EAAE,QAAqB;QAC1D,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACpC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;YAChB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACjC,CAAC;QACD,wEAAwE;QACvE,GAAwB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAExC,OAAO,GAAG,EAAE;YACT,GAAwB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC7C,CAAC,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,IAAI,CAA2B,KAAQ,EAAE,OAAoB;QAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,KAAK,MAAM,QAAQ,IAAI,GAAG,EAAE,CAAC;YAC1B,QAAwB,CAAC,OAAO,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IAED,wDAAwD;IACxD,IAAI,aAAa;QACf,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,CAAC;YAC1C,KAAK,IAAI,GAAG,CAAC,IAAI,CAAC;QACpB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}