@letta-ai/dreams 0.0.1 → 0.0.4
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 +111 -35
- package/bin/dreams-codex-hook.cjs +91 -0
- package/bin/dreams.cjs +21 -0
- package/dist/auth/commands.js +67 -5
- package/dist/auth/credentials.js +101 -22
- package/dist/auth/index.js +106 -13
- package/dist/cli.js +239 -13
- package/dist/cloud/client.js +89 -5
- package/dist/cloud/upload.js +150 -70
- package/dist/development-reset.js +359 -0
- package/dist/launcher.js +311 -0
- package/dist/local/backfill.js +625 -0
- package/dist/local/bindings.js +30 -0
- package/dist/local/claude-hooks.js +169 -0
- package/dist/local/claude.js +130 -176
- package/dist/local/codex-hooks.js +235 -0
- package/dist/local/codex.js +510 -0
- package/dist/local/commands.js +153 -19
- package/dist/local/db.js +68 -0
- package/dist/local/detect.js +148 -0
- package/dist/local/discovered.js +6 -0
- package/dist/local/hooks-install.js +102 -0
- package/dist/local/leases.js +3 -1
- package/dist/local/scan.js +99 -17
- package/dist/local/source-adapter.js +8 -0
- package/dist/local/source-adapters.js +71 -0
- package/dist/local/state-lock.js +88 -0
- package/dist/local/sync-control.js +432 -0
- package/dist/local/transcript-bytes.js +204 -0
- package/dist/managed-install.js +74 -86
- package/dist/onboard.js +26 -72
- package/dist/skill.js +373 -16
- package/dist/snapshots.js +210 -0
- package/dist/suppress-sqlite-warning.js +34 -0
- package/dist/sync.js +950 -0
- package/package.json +1 -1
package/dist/sync.js
ADDED
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Hook-safe `dreams sync` / `dreams wake` ([LET-10237] / [LET-10240]).
|
|
4
|
+
*
|
|
5
|
+
* - `dreams sync` (Stop / live turn): records a durable trigger (optional
|
|
6
|
+
* session_id from stdin) and may spawn a detached worker.
|
|
7
|
+
* - `dreams wake` (SessionStart): quiet housekeeping (managed CLI + skill
|
|
8
|
+
* repair) then force-kicks the worker for pending/backfill drain — no stdin
|
|
9
|
+
* session hint.
|
|
10
|
+
*
|
|
11
|
+
* The hook parent always exits 0. The worker performs live scan + live-first
|
|
12
|
+
* upload, then — only when an active backfill policy already exists and upload
|
|
13
|
+
* budget remains — one backfill continuation slice + remaining-budget upload.
|
|
14
|
+
* Historical windows are never minted here; use `dreams backfill start`.
|
|
15
|
+
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.resolveSyncAdapter = resolveSyncAdapter;
|
|
51
|
+
exports.setDetachedWorkerSpawnerForTests = setDetachedWorkerSpawnerForTests;
|
|
52
|
+
exports.setBeforeWorkerLeaseReleaseForTests = setBeforeWorkerLeaseReleaseForTests;
|
|
53
|
+
exports.readSessionHintFromStdin = readSessionHintFromStdin;
|
|
54
|
+
exports.commandSyncSchedule = commandSyncSchedule;
|
|
55
|
+
exports.commandSyncPause = commandSyncPause;
|
|
56
|
+
exports.commandSyncResume = commandSyncResume;
|
|
57
|
+
exports.commandSyncStatus = commandSyncStatus;
|
|
58
|
+
exports.commandSyncWorker = commandSyncWorker;
|
|
59
|
+
exports.commandWake = commandWake;
|
|
60
|
+
exports.commandSync = commandSync;
|
|
61
|
+
const node_child_process_1 = require("node:child_process");
|
|
62
|
+
const path = __importStar(require("node:path"));
|
|
63
|
+
const upload_1 = require("./cloud/upload");
|
|
64
|
+
const backfill_1 = require("./local/backfill");
|
|
65
|
+
const db_1 = require("./local/db");
|
|
66
|
+
const leases_1 = require("./local/leases");
|
|
67
|
+
const scan_1 = require("./local/scan");
|
|
68
|
+
const state_lock_1 = require("./local/state-lock");
|
|
69
|
+
const sync_control_1 = require("./local/sync-control");
|
|
70
|
+
const codex_1 = require("./local/codex");
|
|
71
|
+
const source_adapters_1 = require("./local/source-adapters");
|
|
72
|
+
const managed_install_1 = require("./managed-install");
|
|
73
|
+
const skill_1 = require("./skill");
|
|
74
|
+
const store_1 = require("./store");
|
|
75
|
+
const STDIN_LIMIT_BYTES = 64 * 1024;
|
|
76
|
+
/** Resolve `--source <type>` at the sync command boundary (stdin never selects source). */
|
|
77
|
+
function resolveSyncAdapter(sourceType) {
|
|
78
|
+
return (0, source_adapters_1.resolveSourceAdapter)(sourceType);
|
|
79
|
+
}
|
|
80
|
+
function defaultWorkerEntry() {
|
|
81
|
+
const candidate = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
82
|
+
// Only trust argv when this process was started via the CLI entrypoints.
|
|
83
|
+
// Under `node --test`, argv[1] is the test file and must not be respawned.
|
|
84
|
+
if (candidate && /(?:^|[/\\])(?:cli\.(?:js|ts)|dreams\.cjs)$/.test(candidate))
|
|
85
|
+
return candidate;
|
|
86
|
+
return path.resolve(__dirname, "..", "bin", "dreams.cjs");
|
|
87
|
+
}
|
|
88
|
+
function defaultSpawnDetachedWorker(force, sourceType) {
|
|
89
|
+
try {
|
|
90
|
+
const args = [defaultWorkerEntry(), "sync", "--worker", "--json"];
|
|
91
|
+
if (force)
|
|
92
|
+
args.push("--force");
|
|
93
|
+
const type = sourceType ?? (0, source_adapters_1.defaultSourceAdapter)().sourceType;
|
|
94
|
+
args.push("--source", type);
|
|
95
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, args, {
|
|
96
|
+
detached: true,
|
|
97
|
+
stdio: "ignore",
|
|
98
|
+
shell: false,
|
|
99
|
+
env: process.env,
|
|
100
|
+
});
|
|
101
|
+
// Asynchronous spawn failures must not reject the hook parent.
|
|
102
|
+
child.on("error", () => {
|
|
103
|
+
/* leave durable trigger pending for a later wake */
|
|
104
|
+
});
|
|
105
|
+
child.unref();
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
let detachedWorkerSpawner = defaultSpawnDetachedWorker;
|
|
113
|
+
/** Test-only: replace (or restore) the detached worker spawner. */
|
|
114
|
+
function setDetachedWorkerSpawnerForTests(spawner) {
|
|
115
|
+
detachedWorkerSpawner = spawner ?? defaultSpawnDetachedWorker;
|
|
116
|
+
}
|
|
117
|
+
/** Test-only: run after the in-loop successor decision, before lease release. */
|
|
118
|
+
let beforeWorkerLeaseReleaseForTests = null;
|
|
119
|
+
function setBeforeWorkerLeaseReleaseForTests(hook) {
|
|
120
|
+
beforeWorkerLeaseReleaseForTests = hook;
|
|
121
|
+
}
|
|
122
|
+
function spawnDetachedWorker(force, sourceType) {
|
|
123
|
+
return detachedWorkerSpawner(force, sourceType);
|
|
124
|
+
}
|
|
125
|
+
function emit(json, payload, human) {
|
|
126
|
+
if (json)
|
|
127
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
128
|
+
else
|
|
129
|
+
console.log(human());
|
|
130
|
+
}
|
|
131
|
+
function openInstallationDb() {
|
|
132
|
+
const installation = (0, store_1.loadOrCreateInstallation)();
|
|
133
|
+
const db = (0, db_1.openDb)({
|
|
134
|
+
installationId: installation.installation_id,
|
|
135
|
+
installationCreatedAt: installation.created_at,
|
|
136
|
+
});
|
|
137
|
+
return { db, installationId: installation.installation_id };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Bound stdin read for hook payloads.
|
|
141
|
+
* Ignore interactive TTYs. In non-TTY environments without a real pipe (tests),
|
|
142
|
+
* time out quickly so we never hang waiting for EOF.
|
|
143
|
+
*/
|
|
144
|
+
async function readSessionHintFromStdin(adapter, timeoutMs = 100) {
|
|
145
|
+
if (process.stdin.isTTY)
|
|
146
|
+
return null;
|
|
147
|
+
const chunks = [];
|
|
148
|
+
let total = 0;
|
|
149
|
+
const text = await new Promise((resolve) => {
|
|
150
|
+
let settled = false;
|
|
151
|
+
const finish = (value) => {
|
|
152
|
+
if (settled)
|
|
153
|
+
return;
|
|
154
|
+
settled = true;
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
process.stdin.removeListener("data", onData);
|
|
157
|
+
process.stdin.removeListener("end", onEnd);
|
|
158
|
+
process.stdin.removeListener("error", onError);
|
|
159
|
+
// Pause so a non-EOF stdin (test runners / sockets) does not keep the
|
|
160
|
+
// event loop alive after the bounded read completes.
|
|
161
|
+
try {
|
|
162
|
+
process.stdin.pause();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// ignore
|
|
166
|
+
}
|
|
167
|
+
resolve(value);
|
|
168
|
+
};
|
|
169
|
+
const onData = (chunk) => {
|
|
170
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
171
|
+
total += buf.length;
|
|
172
|
+
if (total <= STDIN_LIMIT_BYTES)
|
|
173
|
+
chunks.push(buf);
|
|
174
|
+
if (total >= STDIN_LIMIT_BYTES)
|
|
175
|
+
finish(Buffer.concat(chunks).toString("utf8"));
|
|
176
|
+
};
|
|
177
|
+
const onEnd = () => finish(chunks.length ? Buffer.concat(chunks).toString("utf8") : null);
|
|
178
|
+
const onError = () => finish(null);
|
|
179
|
+
const timer = setTimeout(() => finish(chunks.length ? Buffer.concat(chunks).toString("utf8") : null), timeoutMs);
|
|
180
|
+
process.stdin.on("data", onData);
|
|
181
|
+
process.stdin.on("end", onEnd);
|
|
182
|
+
process.stdin.on("error", onError);
|
|
183
|
+
if (process.stdin.readableEnded)
|
|
184
|
+
onEnd();
|
|
185
|
+
else
|
|
186
|
+
process.stdin.resume();
|
|
187
|
+
});
|
|
188
|
+
if (!text)
|
|
189
|
+
return null;
|
|
190
|
+
const trimmed = text.trim();
|
|
191
|
+
if (!trimmed)
|
|
192
|
+
return null;
|
|
193
|
+
try {
|
|
194
|
+
const parsed = JSON.parse(trimmed);
|
|
195
|
+
// Only session_id is trusted as a live hint. transcript_path / cwd from hooks
|
|
196
|
+
// are informational and must not become authoritative discovery paths.
|
|
197
|
+
const candidates = [parsed.session_id, parsed.sessionId, parsed.sessionID];
|
|
198
|
+
for (const value of candidates) {
|
|
199
|
+
if (typeof value === "string" && adapter.validateSessionId(value))
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// Malformed hook stdin is ignored; sync still proceeds for tracked sessions.
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
function shouldSpawnWorker(options) {
|
|
209
|
+
if (options.forced)
|
|
210
|
+
return true;
|
|
211
|
+
if (options.retryDue)
|
|
212
|
+
return true;
|
|
213
|
+
if (options.runtimeDue && options.pending)
|
|
214
|
+
return true;
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
async function commandSyncSchedule(options) {
|
|
218
|
+
// Hook parent must always exit 0 — never surface local/SQLite failures as exit 1.
|
|
219
|
+
const adapter = options.adapter;
|
|
220
|
+
const sourceId = adapter.sourceId;
|
|
221
|
+
try {
|
|
222
|
+
const sessionHint = options.skipStdin ? null : await readSessionHintFromStdin(adapter);
|
|
223
|
+
const { db } = openInstallationDb();
|
|
224
|
+
try {
|
|
225
|
+
const leaseRow = db.prepare("SELECT owner, expires_at FROM leases WHERE name = ?").get(leases_1.SYNC_WORKER_LEASE);
|
|
226
|
+
const workerActive = Boolean(leaseRow && leaseRow.expires_at > Date.now());
|
|
227
|
+
(0, sync_control_1.normalizeStaleRunning)(db, workerActive);
|
|
228
|
+
const recorded = (0, sync_control_1.recordSyncRequest)(db, { forced: options.force, sessionHint, sourceId });
|
|
229
|
+
if (recorded.paused) {
|
|
230
|
+
emit(options.json, { status: "paused", schema_version: 1 }, () => "Sync paused; hooks are no-ops until `dreams sync resume`.");
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
const runtime = (0, sync_control_1.readRuntime)(db, sourceId);
|
|
234
|
+
const pending = (0, sync_control_1.triggerPending)(db, sourceId);
|
|
235
|
+
const due = options.force || recorded.forced || (0, sync_control_1.cadenceDue)(runtime) || (0, sync_control_1.retryDueFromError)(runtime);
|
|
236
|
+
if (!due && !workerActive) {
|
|
237
|
+
// Do not advance processed_gen here. Consuming on not_due would drop a
|
|
238
|
+
// durable trigger left pending after spawn failure / pass-budget handoff.
|
|
239
|
+
emit(options.json, {
|
|
240
|
+
status: "not_due",
|
|
241
|
+
schema_version: 1,
|
|
242
|
+
source: adapter.sourceType,
|
|
243
|
+
trigger_generation: recorded.generation,
|
|
244
|
+
pending,
|
|
245
|
+
next_due_at: runtime.next_due_at,
|
|
246
|
+
cadence_ms: sync_control_1.CADENCE_MS,
|
|
247
|
+
}, () => `Sync not due until ${runtime.next_due_at ?? "now"}.`);
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
if (workerActive) {
|
|
251
|
+
emit(options.json, {
|
|
252
|
+
status: "coalesced",
|
|
253
|
+
schema_version: 1,
|
|
254
|
+
source: adapter.sourceType,
|
|
255
|
+
trigger_generation: recorded.generation,
|
|
256
|
+
forced: recorded.forced,
|
|
257
|
+
}, () => `Sync request coalesced into the running worker (generation ${recorded.generation}).`);
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
260
|
+
if (!shouldSpawnWorker({
|
|
261
|
+
forced: options.force || recorded.forced,
|
|
262
|
+
runtimeDue: (0, sync_control_1.cadenceDue)(runtime),
|
|
263
|
+
retryDue: (0, sync_control_1.retryDueFromError)(runtime),
|
|
264
|
+
pending,
|
|
265
|
+
})) {
|
|
266
|
+
emit(options.json, {
|
|
267
|
+
status: "coalesced",
|
|
268
|
+
schema_version: 1,
|
|
269
|
+
source: adapter.sourceType,
|
|
270
|
+
trigger_generation: recorded.generation,
|
|
271
|
+
forced: recorded.forced,
|
|
272
|
+
}, () => `Sync request recorded (generation ${recorded.generation}).`);
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
const spawned = spawnDetachedWorker(options.force || recorded.forced, adapter.sourceType);
|
|
276
|
+
// Spawn failure must leave the durable trigger pending for a later wake
|
|
277
|
+
// and record a degraded diagnosis visible via `sync status`.
|
|
278
|
+
if (!spawned) {
|
|
279
|
+
(0, sync_control_1.markRuntimeDegraded)(db, "spawn_failed", "worker could not be detached; trigger left pending for next wake", null, sourceId);
|
|
280
|
+
}
|
|
281
|
+
emit(options.json, {
|
|
282
|
+
status: spawned ? "scheduled" : "coalesced",
|
|
283
|
+
schema_version: 1,
|
|
284
|
+
source: adapter.sourceType,
|
|
285
|
+
trigger_generation: recorded.generation,
|
|
286
|
+
forced: recorded.forced,
|
|
287
|
+
worker: spawned ? "spawned" : "spawn_failed",
|
|
288
|
+
}, () => spawned
|
|
289
|
+
? `Sync scheduled (generation ${recorded.generation}).`
|
|
290
|
+
: `Sync request recorded; worker spawn failed — will retry on next wake (generation ${recorded.generation}).`);
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
(0, db_1.closeDb)();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
try {
|
|
299
|
+
emit(options.json, {
|
|
300
|
+
status: "coalesced",
|
|
301
|
+
schema_version: 1,
|
|
302
|
+
source: adapter.sourceType,
|
|
303
|
+
worker: "schedule_error",
|
|
304
|
+
message: error instanceof Error ? error.message : String(error),
|
|
305
|
+
}, () => "Sync request could not be fully scheduled; will retry on next wake.");
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// ignore secondary emit failures
|
|
309
|
+
}
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function commandSyncPause(json) {
|
|
314
|
+
const { db } = openInstallationDb();
|
|
315
|
+
try {
|
|
316
|
+
const runtime = (0, sync_control_1.setPaused)(db, true);
|
|
317
|
+
emit(json, { status: "paused", schema_version: 1, sync: runtime }, () => "Sync paused. Existing hooks will no-op.");
|
|
318
|
+
return 0;
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
(0, db_1.closeDb)();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function commandSyncResume(json) {
|
|
325
|
+
const { db } = openInstallationDb();
|
|
326
|
+
try {
|
|
327
|
+
const runtime = (0, sync_control_1.setPaused)(db, false);
|
|
328
|
+
// Resume force-wakes every source with a trigger row; spawn the first
|
|
329
|
+
// runnable source and let post-pass handoff chain the rest ([LET-10314]).
|
|
330
|
+
const nextId = (0, sync_control_1.nextRunnableSourceId)(db) ?? (0, source_adapters_1.defaultSourceAdapter)().sourceId;
|
|
331
|
+
const nextType = (0, source_adapters_1.getSourceAdapterById)(nextId).sourceType;
|
|
332
|
+
const spawned = spawnDetachedWorker(true, nextType);
|
|
333
|
+
emit(json, { status: "resumed", schema_version: 1, sync: runtime, worker: spawned ? "scheduled" : "pending", source: nextType }, () => "Sync resumed; an immediate sync was requested.");
|
|
334
|
+
return 0;
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
(0, db_1.closeDb)();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function commandSyncStatus(json, sourceType) {
|
|
341
|
+
let adapter;
|
|
342
|
+
try {
|
|
343
|
+
adapter = resolveSyncAdapter(sourceType);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
347
|
+
if (json) {
|
|
348
|
+
console.log(JSON.stringify({ status: "error", schema_version: 1, code: "invalid_argument", message }));
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
process.stderr.write(`${message}\n`);
|
|
352
|
+
}
|
|
353
|
+
return 1;
|
|
354
|
+
}
|
|
355
|
+
const harnessPresent = adapter.detect().present;
|
|
356
|
+
const { db } = openInstallationDb();
|
|
357
|
+
try {
|
|
358
|
+
const configured = (0, sync_control_1.sourceExists)(db, adapter.sourceId);
|
|
359
|
+
const leaseRow = db.prepare("SELECT expires_at FROM leases WHERE name = ?").get(leases_1.SYNC_WORKER_LEASE);
|
|
360
|
+
const workerLeaseHeld = Boolean(leaseRow && leaseRow.expires_at > Date.now());
|
|
361
|
+
(0, sync_control_1.normalizeStaleRunning)(db, workerLeaseHeld);
|
|
362
|
+
// Peek only — status must not mint source/trigger rows for unconfigured adapters.
|
|
363
|
+
const runtime = (0, sync_control_1.peekRuntime)(db, adapter.sourceId);
|
|
364
|
+
const trigger = (0, sync_control_1.observeTrigger)(db, adapter.sourceId);
|
|
365
|
+
const pending = configured && (trigger.requested_gen > trigger.processed_gen || trigger.forced);
|
|
366
|
+
const workerActiveForSource = workerLeaseHeld && configured && (0, sync_control_1.readActiveSourceId)(db) === adapter.sourceId;
|
|
367
|
+
const effective = configured
|
|
368
|
+
? (0, sync_control_1.effectiveSyncState)({
|
|
369
|
+
runtime,
|
|
370
|
+
pending,
|
|
371
|
+
forced: trigger.forced,
|
|
372
|
+
workerActive: workerActiveForSource,
|
|
373
|
+
})
|
|
374
|
+
: runtime.paused
|
|
375
|
+
? "paused"
|
|
376
|
+
: "idle";
|
|
377
|
+
const discovery = adapter.sourceType === codex_1.CODEX_SOURCE_TYPE
|
|
378
|
+
? (() => {
|
|
379
|
+
const report = (0, codex_1.inspectCodexDiscovery)();
|
|
380
|
+
return {
|
|
381
|
+
status: report.status === "warning" ? "warning" : report.status === "ok" ? "ok" : report.status,
|
|
382
|
+
harness_present: report.harness_present,
|
|
383
|
+
sessions_dir_present: report.sessions_dir_present,
|
|
384
|
+
candidates: report.candidates,
|
|
385
|
+
supported: report.supported,
|
|
386
|
+
unsupported: report.unsupported,
|
|
387
|
+
excluded_subagent: report.excluded_subagent,
|
|
388
|
+
by_reason: report.by_reason,
|
|
389
|
+
examples: report.examples,
|
|
390
|
+
message: report.message,
|
|
391
|
+
};
|
|
392
|
+
})()
|
|
393
|
+
: {
|
|
394
|
+
status: harnessPresent ? "ok" : "absent",
|
|
395
|
+
harness_present: harnessPresent,
|
|
396
|
+
message: harnessPresent ? null : `${adapter.displayName} was not detected`,
|
|
397
|
+
};
|
|
398
|
+
const payload = {
|
|
399
|
+
schema_version: 1,
|
|
400
|
+
status: "ok",
|
|
401
|
+
source: adapter.sourceType,
|
|
402
|
+
source_id: adapter.sourceId,
|
|
403
|
+
source_configured: configured,
|
|
404
|
+
source_present: harnessPresent,
|
|
405
|
+
sync: {
|
|
406
|
+
...runtime,
|
|
407
|
+
effective_state: effective,
|
|
408
|
+
worker_active: workerActiveForSource,
|
|
409
|
+
worker_lease_held: workerLeaseHeld,
|
|
410
|
+
// Stale ownership must not outlive the lease (belt-and-suspenders with completeSyncPass).
|
|
411
|
+
active_source_id: workerLeaseHeld ? (0, sync_control_1.readActiveSourceId)(db) : null,
|
|
412
|
+
pending,
|
|
413
|
+
requested_gen: trigger.requested_gen,
|
|
414
|
+
processed_gen: trigger.processed_gen,
|
|
415
|
+
forced: trigger.forced,
|
|
416
|
+
session_hints: trigger.session_hints,
|
|
417
|
+
cadence_ms: sync_control_1.CADENCE_MS,
|
|
418
|
+
},
|
|
419
|
+
discovery,
|
|
420
|
+
};
|
|
421
|
+
emit(json, payload, () => [
|
|
422
|
+
"Dreams sync status",
|
|
423
|
+
"",
|
|
424
|
+
` Source: ${adapter.sourceType} (${adapter.sourceId})${configured ? "" : " — not configured"}`,
|
|
425
|
+
` Effective: ${effective}`,
|
|
426
|
+
` Runtime: ${runtime.state}`,
|
|
427
|
+
` Paused: ${runtime.paused ? "yes" : "no"}`,
|
|
428
|
+
` Pending: ${pending ? "yes" : "no"}`,
|
|
429
|
+
` Worker: ${workerActiveForSource ? "active for this source" : workerLeaseHeld ? "active for another source" : "idle"}`,
|
|
430
|
+
` Next due: ${runtime.next_due_at ?? "(immediately)"}`,
|
|
431
|
+
` Last success:${runtime.last_success_at ? ` ${runtime.last_success_at}` : " (none)"}`,
|
|
432
|
+
` Last error: ${runtime.last_error_code ? `${runtime.last_error_code}: ${runtime.last_error_message}` : " (none)"}`,
|
|
433
|
+
discovery.message ? ` Discovery: ${discovery.message}` : "",
|
|
434
|
+
]
|
|
435
|
+
.filter((line) => line !== "")
|
|
436
|
+
.join("\n"));
|
|
437
|
+
return 0;
|
|
438
|
+
}
|
|
439
|
+
finally {
|
|
440
|
+
(0, db_1.closeDb)();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function uploadFailureResult(db, uploadSummary, budgetExhausted, backfillContinuation, sourceId) {
|
|
444
|
+
if (uploadSummary.stopped_code) {
|
|
445
|
+
return {
|
|
446
|
+
ok: false,
|
|
447
|
+
code: uploadSummary.stopped_code,
|
|
448
|
+
message: uploadSummary.stopped_reason,
|
|
449
|
+
budgetExhausted,
|
|
450
|
+
backfillContinuation,
|
|
451
|
+
retryAt: (0, sync_control_1.earliestUploadRetryAt)(db, sourceId) ?? (0, sync_control_1.fallbackRetryAt)(uploadSummary.stopped_code),
|
|
452
|
+
paused: false,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
if (uploadSummary.retry_scheduled > 0) {
|
|
456
|
+
return {
|
|
457
|
+
ok: false,
|
|
458
|
+
code: "network",
|
|
459
|
+
message: `retry_scheduled=${uploadSummary.retry_scheduled}`,
|
|
460
|
+
budgetExhausted,
|
|
461
|
+
backfillContinuation,
|
|
462
|
+
retryAt: (0, sync_control_1.earliestUploadRetryAt)(db, sourceId) ?? (0, sync_control_1.fallbackRetryAt)("network"),
|
|
463
|
+
paused: false,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
if (uploadSummary.partial && uploadSummary.quarantined > 0 && uploadSummary.accepted === 0 && uploadSummary.duplicate === 0) {
|
|
467
|
+
return {
|
|
468
|
+
ok: false,
|
|
469
|
+
code: "local_error",
|
|
470
|
+
message: "upload pass quarantined rows without accepts",
|
|
471
|
+
budgetExhausted,
|
|
472
|
+
backfillContinuation,
|
|
473
|
+
retryAt: (0, sync_control_1.fallbackRetryAt)("local_error"),
|
|
474
|
+
paused: false,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
async function runOneWorkerPass(db, installationId, sourceOwner, workerOwner, observed, adapter) {
|
|
480
|
+
if ((0, sync_control_1.isPaused)(db)) {
|
|
481
|
+
return {
|
|
482
|
+
ok: true,
|
|
483
|
+
code: null,
|
|
484
|
+
message: null,
|
|
485
|
+
budgetExhausted: false,
|
|
486
|
+
backfillContinuation: false,
|
|
487
|
+
retryAt: null,
|
|
488
|
+
paused: true,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const heartbeat = () => {
|
|
492
|
+
if (!(0, leases_1.renewLease)(db, leases_1.SYNC_WORKER_LEASE, workerOwner))
|
|
493
|
+
throw new Error("sync-worker lease lost");
|
|
494
|
+
};
|
|
495
|
+
const cancel = () => (0, sync_control_1.isPaused)(db);
|
|
496
|
+
const totalBudget = (0, sync_control_1.uploadRowBudget)();
|
|
497
|
+
const scanSummary = (0, scan_1.reconcile)(db, {
|
|
498
|
+
installationId,
|
|
499
|
+
leaseOwner: sourceOwner,
|
|
500
|
+
adapter,
|
|
501
|
+
mode: "live",
|
|
502
|
+
hintSessionIds: observed.session_hints,
|
|
503
|
+
priority: "live",
|
|
504
|
+
shouldCancel: cancel,
|
|
505
|
+
onProgress: heartbeat,
|
|
506
|
+
});
|
|
507
|
+
if ((0, sync_control_1.isPaused)(db)) {
|
|
508
|
+
return {
|
|
509
|
+
ok: true,
|
|
510
|
+
code: null,
|
|
511
|
+
message: null,
|
|
512
|
+
budgetExhausted: false,
|
|
513
|
+
backfillContinuation: false,
|
|
514
|
+
retryAt: null,
|
|
515
|
+
paused: true,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
heartbeat();
|
|
519
|
+
const liveUpload = await (0, upload_1.runUpload)(db, {
|
|
520
|
+
installationId,
|
|
521
|
+
leaseOwner: sourceOwner,
|
|
522
|
+
adapter,
|
|
523
|
+
maxRows: totalBudget,
|
|
524
|
+
shouldCancel: cancel,
|
|
525
|
+
onProgress: heartbeat,
|
|
526
|
+
});
|
|
527
|
+
if ((0, sync_control_1.isPaused)(db)) {
|
|
528
|
+
return {
|
|
529
|
+
ok: true,
|
|
530
|
+
code: null,
|
|
531
|
+
message: null,
|
|
532
|
+
budgetExhausted: liveUpload.budget_exhausted,
|
|
533
|
+
backfillContinuation: false,
|
|
534
|
+
retryAt: null,
|
|
535
|
+
paused: true,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
const liveFail = uploadFailureResult(db, liveUpload, liveUpload.budget_exhausted, false, adapter.sourceId);
|
|
539
|
+
if (liveFail)
|
|
540
|
+
return liveFail;
|
|
541
|
+
let rowsProcessed = liveUpload.rows_processed;
|
|
542
|
+
let budgetExhausted = liveUpload.budget_exhausted;
|
|
543
|
+
let accepted = liveUpload.accepted;
|
|
544
|
+
let duplicate = liveUpload.duplicate;
|
|
545
|
+
let backfillSessions = 0;
|
|
546
|
+
let backfillEnqueued = 0;
|
|
547
|
+
let backfillAccepted = 0;
|
|
548
|
+
let backfillContinuation = false;
|
|
549
|
+
// Live-first: only continue an existing active policy when shared budget remains.
|
|
550
|
+
const remainingBudget = Math.max(0, totalBudget - rowsProcessed);
|
|
551
|
+
if (!budgetExhausted && remainingBudget > 0) {
|
|
552
|
+
const backfill = (0, backfill_1.runBackfillContinuePass)(db, {
|
|
553
|
+
installationId,
|
|
554
|
+
leaseOwner: sourceOwner,
|
|
555
|
+
adapter,
|
|
556
|
+
shouldCancel: cancel,
|
|
557
|
+
onProgress: heartbeat,
|
|
558
|
+
});
|
|
559
|
+
if ((0, sync_control_1.isPaused)(db) || backfill.canceled) {
|
|
560
|
+
return {
|
|
561
|
+
ok: true,
|
|
562
|
+
code: null,
|
|
563
|
+
message: null,
|
|
564
|
+
budgetExhausted,
|
|
565
|
+
backfillContinuation: false,
|
|
566
|
+
retryAt: null,
|
|
567
|
+
paused: true,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
backfillSessions = backfill.sessions_selected;
|
|
571
|
+
backfillEnqueued = backfill.scan.segments_enqueued;
|
|
572
|
+
if (backfill.status !== "skipped") {
|
|
573
|
+
heartbeat();
|
|
574
|
+
const backfillUpload = await (0, upload_1.runUpload)(db, {
|
|
575
|
+
installationId,
|
|
576
|
+
leaseOwner: sourceOwner,
|
|
577
|
+
adapter,
|
|
578
|
+
maxRows: remainingBudget,
|
|
579
|
+
shouldCancel: cancel,
|
|
580
|
+
onProgress: heartbeat,
|
|
581
|
+
});
|
|
582
|
+
if ((0, sync_control_1.isPaused)(db)) {
|
|
583
|
+
return {
|
|
584
|
+
ok: true,
|
|
585
|
+
code: null,
|
|
586
|
+
message: null,
|
|
587
|
+
budgetExhausted: budgetExhausted || backfillUpload.budget_exhausted,
|
|
588
|
+
backfillContinuation: false,
|
|
589
|
+
retryAt: null,
|
|
590
|
+
paused: true,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
rowsProcessed += backfillUpload.rows_processed;
|
|
594
|
+
budgetExhausted = budgetExhausted || backfillUpload.budget_exhausted;
|
|
595
|
+
accepted += backfillUpload.accepted;
|
|
596
|
+
duplicate += backfillUpload.duplicate;
|
|
597
|
+
backfillAccepted = backfillUpload.accepted + backfillUpload.duplicate;
|
|
598
|
+
const backfillMadeProgress = backfill.made_progress || backfillAccepted > 0 || backfillEnqueued > 0;
|
|
599
|
+
backfillContinuation = backfillMadeProgress && backfill.actionable_remaining > 0;
|
|
600
|
+
const backfillFail = uploadFailureResult(db, backfillUpload, budgetExhausted,
|
|
601
|
+
// Keep continuation intent even on retryable upload failure when frontier remains.
|
|
602
|
+
backfillContinuation, adapter.sourceId);
|
|
603
|
+
if (backfillFail)
|
|
604
|
+
return backfillFail;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
ok: true,
|
|
609
|
+
code: null,
|
|
610
|
+
message: `live_sessions=${scanSummary.sessions_queued}; backfill_sessions=${backfillSessions}; accepted=${accepted}; duplicate=${duplicate}; rows_processed=${rowsProcessed}`,
|
|
611
|
+
budgetExhausted,
|
|
612
|
+
backfillContinuation,
|
|
613
|
+
retryAt: null,
|
|
614
|
+
paused: false,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
async function commandSyncWorker(options) {
|
|
618
|
+
const adapter = options.adapter;
|
|
619
|
+
const sourceId = adapter.sourceId;
|
|
620
|
+
const lock = (0, state_lock_1.acquireLocalStateLock)();
|
|
621
|
+
if (lock === null) {
|
|
622
|
+
// Leave trigger pending for a later wake.
|
|
623
|
+
if (options.json) {
|
|
624
|
+
console.log(JSON.stringify({ status: "busy", schema_version: 1, code: "busy", source: adapter.sourceType }));
|
|
625
|
+
}
|
|
626
|
+
return 0;
|
|
627
|
+
}
|
|
628
|
+
const { db, installationId } = openInstallationDb();
|
|
629
|
+
let workerOwner = null;
|
|
630
|
+
let sourceOwner = null;
|
|
631
|
+
let handoffEligible = false;
|
|
632
|
+
let passes = 0;
|
|
633
|
+
let lastResult = {
|
|
634
|
+
ok: true,
|
|
635
|
+
code: null,
|
|
636
|
+
message: null,
|
|
637
|
+
budgetExhausted: false,
|
|
638
|
+
backfillContinuation: false,
|
|
639
|
+
retryAt: null,
|
|
640
|
+
paused: false,
|
|
641
|
+
};
|
|
642
|
+
try {
|
|
643
|
+
workerOwner = (0, leases_1.acquireLease)(db, leases_1.SYNC_WORKER_LEASE);
|
|
644
|
+
if (workerOwner === null) {
|
|
645
|
+
if (options.json) {
|
|
646
|
+
console.log(JSON.stringify({ status: "busy", schema_version: 1, code: "busy", source: adapter.sourceType }));
|
|
647
|
+
}
|
|
648
|
+
return 0;
|
|
649
|
+
}
|
|
650
|
+
(0, sync_control_1.normalizeStaleRunning)(db, true);
|
|
651
|
+
if ((0, sync_control_1.isPaused)(db)) {
|
|
652
|
+
if (options.json)
|
|
653
|
+
console.log(JSON.stringify({ status: "paused", schema_version: 1, source: adapter.sourceType }));
|
|
654
|
+
return 0;
|
|
655
|
+
}
|
|
656
|
+
(0, sync_control_1.markRuntimeRunning)(db, sourceId);
|
|
657
|
+
while (passes < sync_control_1.MAX_TRAILING_PASSES) {
|
|
658
|
+
if ((0, sync_control_1.isPaused)(db))
|
|
659
|
+
break;
|
|
660
|
+
const observed = (0, sync_control_1.observeTrigger)(db, sourceId);
|
|
661
|
+
const runtime = (0, sync_control_1.readRuntime)(db, sourceId);
|
|
662
|
+
const due = options.force ||
|
|
663
|
+
observed.forced ||
|
|
664
|
+
observed.requested_gen > observed.processed_gen ||
|
|
665
|
+
(0, sync_control_1.cadenceDue)(runtime) ||
|
|
666
|
+
(0, sync_control_1.retryDueFromError)(runtime);
|
|
667
|
+
if (!due && passes > 0)
|
|
668
|
+
break;
|
|
669
|
+
if (!due && passes === 0 && !options.force && !observed.forced) {
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
sourceOwner = (0, leases_1.acquireLease)(db, leases_1.SOURCE_LEASE);
|
|
673
|
+
if (sourceOwner === null) {
|
|
674
|
+
(0, sync_control_1.markRuntimeDegraded)(db, "busy", "source lease busy; trigger left pending", (0, sync_control_1.fallbackRetryAt)("busy"), sourceId);
|
|
675
|
+
lastResult = {
|
|
676
|
+
ok: false,
|
|
677
|
+
code: "busy",
|
|
678
|
+
message: "source lease busy",
|
|
679
|
+
budgetExhausted: false,
|
|
680
|
+
backfillContinuation: false,
|
|
681
|
+
retryAt: (0, sync_control_1.fallbackRetryAt)("busy"),
|
|
682
|
+
paused: false,
|
|
683
|
+
};
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
if (!(0, leases_1.renewLease)(db, leases_1.SYNC_WORKER_LEASE, workerOwner))
|
|
688
|
+
break;
|
|
689
|
+
lastResult = await runOneWorkerPass(db, installationId, sourceOwner, workerOwner, observed, adapter);
|
|
690
|
+
// Pause mid-pass must not consume the observed generation or its hints.
|
|
691
|
+
if (lastResult.paused || (0, sync_control_1.isPaused)(db)) {
|
|
692
|
+
lastResult = { ...lastResult, paused: true };
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
const keepPending = lastResult.budgetExhausted || lastResult.backfillContinuation;
|
|
696
|
+
const nextDueAt = lastResult.ok ? undefined : (lastResult.retryAt ?? (0, sync_control_1.fallbackRetryAt)(lastResult.code));
|
|
697
|
+
(0, sync_control_1.completeSyncPass)(db, sourceId, observed.requested_gen, {
|
|
698
|
+
ok: lastResult.ok,
|
|
699
|
+
errorCode: lastResult.code,
|
|
700
|
+
errorMessage: lastResult.message,
|
|
701
|
+
advanceCadence: lastResult.ok && !keepPending,
|
|
702
|
+
clearHints: lastResult.ok,
|
|
703
|
+
forcePending: keepPending,
|
|
704
|
+
nextDueAt,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
finally {
|
|
708
|
+
(0, leases_1.releaseLease)(db, leases_1.SOURCE_LEASE, sourceOwner);
|
|
709
|
+
sourceOwner = null;
|
|
710
|
+
}
|
|
711
|
+
passes++;
|
|
712
|
+
if (!(0, sync_control_1.triggerPending)(db, sourceId))
|
|
713
|
+
break;
|
|
714
|
+
// Trailing pass only if generation advanced during the pass.
|
|
715
|
+
if (!(0, sync_control_1.observeTrigger)(db, sourceId).requested_gen ||
|
|
716
|
+
(0, sync_control_1.observeTrigger)(db, sourceId).requested_gen <= observed.requested_gen) {
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
// Immediate successors are only for successful (non-paused) passes. The
|
|
721
|
+
// actual pending check happens after lease release so a coalesced Stop wake
|
|
722
|
+
// that arrives during shutdown is not lost.
|
|
723
|
+
handoffEligible = lastResult.ok && !lastResult.paused;
|
|
724
|
+
if (options.json) {
|
|
725
|
+
console.log(JSON.stringify({
|
|
726
|
+
status: lastResult.paused ? "paused" : lastResult.ok ? "ok" : "degraded",
|
|
727
|
+
schema_version: 1,
|
|
728
|
+
source: adapter.sourceType,
|
|
729
|
+
passes,
|
|
730
|
+
code: lastResult.code,
|
|
731
|
+
message: lastResult.message,
|
|
732
|
+
sync: (0, sync_control_1.readRuntime)(db, sourceId),
|
|
733
|
+
}));
|
|
734
|
+
}
|
|
735
|
+
return 0;
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
try {
|
|
739
|
+
// Do not advance processed_gen — leave in-flight and newer generations pending.
|
|
740
|
+
(0, sync_control_1.markRuntimeDegraded)(db, "local_error", error instanceof Error ? error.message : String(error), (0, sync_control_1.earliestUploadRetryAt)(db, sourceId) ?? (0, sync_control_1.fallbackRetryAt)("local_error"), sourceId);
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
// best effort
|
|
744
|
+
}
|
|
745
|
+
if (options.json) {
|
|
746
|
+
console.log(JSON.stringify({
|
|
747
|
+
status: "degraded",
|
|
748
|
+
schema_version: 1,
|
|
749
|
+
source: adapter.sourceType,
|
|
750
|
+
code: "local_error",
|
|
751
|
+
message: error instanceof Error ? error.message : String(error),
|
|
752
|
+
}));
|
|
753
|
+
}
|
|
754
|
+
handoffEligible = false;
|
|
755
|
+
return 0;
|
|
756
|
+
}
|
|
757
|
+
finally {
|
|
758
|
+
if (sourceOwner)
|
|
759
|
+
(0, leases_1.releaseLease)(db, leases_1.SOURCE_LEASE, sourceOwner);
|
|
760
|
+
try {
|
|
761
|
+
beforeWorkerLeaseReleaseForTests?.(db);
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
// test hooks must not break cleanup
|
|
765
|
+
}
|
|
766
|
+
if (workerOwner) {
|
|
767
|
+
(0, leases_1.releaseLease)(db, leases_1.SYNC_WORKER_LEASE, workerOwner);
|
|
768
|
+
try {
|
|
769
|
+
// Ownership spans trailing passes; clear only when this worker exits.
|
|
770
|
+
(0, sync_control_1.clearActiveSourceId)(db);
|
|
771
|
+
}
|
|
772
|
+
catch {
|
|
773
|
+
// best effort
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
(0, db_1.closeDb)();
|
|
777
|
+
// Release the local-state lock before the final pending check so a hook that
|
|
778
|
+
// spawned during shutdown cannot observe busy-on-lock and exit while we also
|
|
779
|
+
// skip launching a successor.
|
|
780
|
+
(0, state_lock_1.releaseLocalStateLock)(lock);
|
|
781
|
+
let successorType = null;
|
|
782
|
+
if (handoffEligible) {
|
|
783
|
+
try {
|
|
784
|
+
const again = openInstallationDb();
|
|
785
|
+
try {
|
|
786
|
+
// Prefer this source when still pending; otherwise hand off to another
|
|
787
|
+
// registered source with an actionable wake (cross-source coalesce).
|
|
788
|
+
const nextId = (0, sync_control_1.nextRunnableSourceId)(again.db, sourceId);
|
|
789
|
+
if (nextId)
|
|
790
|
+
successorType = (0, source_adapters_1.getSourceAdapterById)(nextId).sourceType;
|
|
791
|
+
}
|
|
792
|
+
finally {
|
|
793
|
+
(0, db_1.closeDb)();
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
successorType = null;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (successorType)
|
|
801
|
+
spawnDetachedWorker(true, successorType);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* SessionStart entrypoint: repair local CLI/skill quietly, then force-schedule
|
|
806
|
+
* the sync worker for pending live/backfill drain (no session stdin hint).
|
|
807
|
+
* Always exits 0 so harness hooks stay non-blocking.
|
|
808
|
+
*/
|
|
809
|
+
async function commandWake(options) {
|
|
810
|
+
let adapter;
|
|
811
|
+
try {
|
|
812
|
+
adapter = resolveSyncAdapter(options.sourceType);
|
|
813
|
+
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
// Still exit 0 for hook safety; surface the error in JSON/human when possible.
|
|
816
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
817
|
+
try {
|
|
818
|
+
emit(options.json, { status: "coalesced", schema_version: 1, command: "wake", worker: "schedule_error", message }, () => `Wake could not resolve source: ${message}`);
|
|
819
|
+
}
|
|
820
|
+
catch {
|
|
821
|
+
// ignore
|
|
822
|
+
}
|
|
823
|
+
return 0;
|
|
824
|
+
}
|
|
825
|
+
const housekeeping = { managed_install: null, skill: null, error: null };
|
|
826
|
+
try {
|
|
827
|
+
const managed = (0, managed_install_1.ensureManagedInstall)();
|
|
828
|
+
housekeeping.managed_install = managed.status;
|
|
829
|
+
housekeeping.skill = (0, skill_1.installSkill)();
|
|
830
|
+
}
|
|
831
|
+
catch (error) {
|
|
832
|
+
housekeeping.error = error instanceof Error ? error.message : String(error);
|
|
833
|
+
}
|
|
834
|
+
// Reuse the sync scheduler with force + no stdin. Emit a wake-shaped envelope
|
|
835
|
+
// by wrapping schedule output when JSON is requested.
|
|
836
|
+
if (!options.json) {
|
|
837
|
+
return commandSyncSchedule({ json: false, force: true, adapter, skipStdin: true });
|
|
838
|
+
}
|
|
839
|
+
// Capture schedule JSON by temporarily patching console.log is brittle; call
|
|
840
|
+
// the schedule path with json:true — it already prints. Prepend housekeeping
|
|
841
|
+
// by running schedule logic inline via a thin re-emit is hard. Instead: run
|
|
842
|
+
// schedule with json false internals... Simplest: run schedule with json true
|
|
843
|
+
// after printing nothing, and document housekeeping in a second object — agents
|
|
844
|
+
// prefer one object. So duplicate the force-schedule emit with housekeeping.
|
|
845
|
+
try {
|
|
846
|
+
const { db } = openInstallationDb();
|
|
847
|
+
try {
|
|
848
|
+
const leaseRow = db.prepare("SELECT owner, expires_at FROM leases WHERE name = ?").get(leases_1.SYNC_WORKER_LEASE);
|
|
849
|
+
const workerActive = Boolean(leaseRow && leaseRow.expires_at > Date.now());
|
|
850
|
+
(0, sync_control_1.normalizeStaleRunning)(db, workerActive);
|
|
851
|
+
const recorded = (0, sync_control_1.recordSyncRequest)(db, { forced: true, sessionHint: null, sourceId: adapter.sourceId });
|
|
852
|
+
if (recorded.paused) {
|
|
853
|
+
emit(true, { status: "paused", schema_version: 1, command: "wake", source: adapter.sourceType, housekeeping }, () => "Wake: sync paused.");
|
|
854
|
+
return 0;
|
|
855
|
+
}
|
|
856
|
+
if (workerActive) {
|
|
857
|
+
emit(true, {
|
|
858
|
+
status: "coalesced",
|
|
859
|
+
schema_version: 1,
|
|
860
|
+
command: "wake",
|
|
861
|
+
source: adapter.sourceType,
|
|
862
|
+
trigger_generation: recorded.generation,
|
|
863
|
+
forced: true,
|
|
864
|
+
housekeeping,
|
|
865
|
+
}, () => "Wake coalesced into running worker.");
|
|
866
|
+
return 0;
|
|
867
|
+
}
|
|
868
|
+
const spawned = spawnDetachedWorker(true, adapter.sourceType);
|
|
869
|
+
if (!spawned) {
|
|
870
|
+
(0, sync_control_1.markRuntimeDegraded)(db, "spawn_failed", "worker could not be detached; trigger left pending for next wake", null, adapter.sourceId);
|
|
871
|
+
}
|
|
872
|
+
emit(true, {
|
|
873
|
+
status: spawned ? "scheduled" : "coalesced",
|
|
874
|
+
schema_version: 1,
|
|
875
|
+
command: "wake",
|
|
876
|
+
source: adapter.sourceType,
|
|
877
|
+
trigger_generation: recorded.generation,
|
|
878
|
+
forced: true,
|
|
879
|
+
worker: spawned ? "spawned" : "spawn_failed",
|
|
880
|
+
housekeeping,
|
|
881
|
+
}, () => (spawned ? "Wake scheduled sync worker." : "Wake recorded; worker spawn failed."));
|
|
882
|
+
return 0;
|
|
883
|
+
}
|
|
884
|
+
finally {
|
|
885
|
+
(0, db_1.closeDb)();
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
catch (error) {
|
|
889
|
+
emit(true, {
|
|
890
|
+
status: "coalesced",
|
|
891
|
+
schema_version: 1,
|
|
892
|
+
command: "wake",
|
|
893
|
+
source: adapter.sourceType,
|
|
894
|
+
worker: "schedule_error",
|
|
895
|
+
message: error instanceof Error ? error.message : String(error),
|
|
896
|
+
housekeeping,
|
|
897
|
+
}, () => "Wake could not fully schedule; will retry later.");
|
|
898
|
+
return 0;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
async function commandSync(subcommand, options) {
|
|
902
|
+
let adapter;
|
|
903
|
+
try {
|
|
904
|
+
adapter = resolveSyncAdapter(options.sourceType);
|
|
905
|
+
}
|
|
906
|
+
catch (error) {
|
|
907
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
908
|
+
return 1;
|
|
909
|
+
}
|
|
910
|
+
if (options.worker) {
|
|
911
|
+
if (subcommand) {
|
|
912
|
+
process.stderr.write(`--worker cannot be combined with sync ${subcommand}\n`);
|
|
913
|
+
return 1;
|
|
914
|
+
}
|
|
915
|
+
return commandSyncWorker({ json: options.json, force: options.force, adapter });
|
|
916
|
+
}
|
|
917
|
+
switch (subcommand) {
|
|
918
|
+
case undefined:
|
|
919
|
+
return commandSyncSchedule({ json: options.json, force: options.force, adapter });
|
|
920
|
+
case "pause":
|
|
921
|
+
if (options.force) {
|
|
922
|
+
process.stderr.write(`--force is not valid with sync pause\n`);
|
|
923
|
+
return 1;
|
|
924
|
+
}
|
|
925
|
+
if (options.sourceType) {
|
|
926
|
+
process.stderr.write(`--source is not valid with sync pause\n`);
|
|
927
|
+
return 1;
|
|
928
|
+
}
|
|
929
|
+
return commandSyncPause(options.json);
|
|
930
|
+
case "resume":
|
|
931
|
+
if (options.force) {
|
|
932
|
+
process.stderr.write(`--force is not valid with sync resume\n`);
|
|
933
|
+
return 1;
|
|
934
|
+
}
|
|
935
|
+
if (options.sourceType) {
|
|
936
|
+
process.stderr.write(`--source is not valid with sync resume\n`);
|
|
937
|
+
return 1;
|
|
938
|
+
}
|
|
939
|
+
return commandSyncResume(options.json);
|
|
940
|
+
case "status":
|
|
941
|
+
if (options.force) {
|
|
942
|
+
process.stderr.write(`--force is not valid with sync status\n`);
|
|
943
|
+
return 1;
|
|
944
|
+
}
|
|
945
|
+
return commandSyncStatus(options.json, options.sourceType);
|
|
946
|
+
default:
|
|
947
|
+
process.stderr.write(`Unknown sync subcommand: ${subcommand} — expected pause, resume, or status\n`);
|
|
948
|
+
return 1;
|
|
949
|
+
}
|
|
950
|
+
}
|