@primitive.ai/prim 0.1.0-alpha.91 → 0.1.0-alpha.93
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 +34 -2
- package/dist/{chunk-67VNLLBN.js → chunk-2GP5UFV7.js} +1 -0
- package/dist/{chunk-VW3FDYRT.js → chunk-36TWVNBE.js} +6 -6
- package/dist/{chunk-HU4PW6I6.js → chunk-C3VAA5CG.js} +1 -1
- package/dist/{chunk-FOLKHXLJ.js → chunk-DCJN6LZH.js} +37 -5
- package/dist/{chunk-M6RIJVRE.js → chunk-DSWT6BCC.js} +78 -4
- package/dist/{chunk-FBAYCS3X.js → chunk-DXJQR4QI.js} +150 -38
- package/dist/{chunk-PCVGBE5B.js → chunk-G2VMA2QO.js} +2 -1
- package/dist/{chunk-NAHHFSLH.js → chunk-ICM2XCXS.js} +2 -2
- package/dist/{chunk-OAAP2MY6.js → chunk-K7D2EYBS.js} +2 -2
- package/dist/{chunk-5AFRMOPX.js → chunk-OH3EZXYX.js} +1 -1
- package/dist/{chunk-KFXF2V5G.js → chunk-OTHKG76J.js} +1 -1
- package/dist/chunk-OYHYCK4K.js +13 -0
- package/dist/{chunk-FKXFUUWJ.js → chunk-TYAHC25F.js} +1 -1
- package/dist/{chunk-5GYQ4J3D.js → chunk-UKUAAAXE.js} +2 -2
- package/dist/{chunk-EWZMDD44.js → chunk-X64HTHUF.js} +22 -3
- package/dist/{chunk-WX52U7Z2.js → chunk-ZU563PQI.js} +58 -2
- package/dist/daemon/server.js +382 -124
- package/dist/hook-runtime/daemon/server.js +382 -124
- package/dist/hook-runtime/hooks/post-commit.js +57 -39
- package/dist/hook-runtime/hooks/post-rewrite.js +58 -40
- package/dist/hook-runtime/hooks/post-tool-use.js +111 -78
- package/dist/hook-runtime/hooks/pre-tool-use.js +48 -26
- package/dist/hook-runtime/hooks/prim-hook.js +157 -117
- package/dist/hook-runtime/hooks/session-end.js +1 -1
- package/dist/hook-runtime/hooks/session-start.js +38 -4
- package/dist/hook-runtime/index.js +1293 -526
- package/dist/hook-runtime/statusline-main.js +23 -1
- package/dist/hooks/post-commit.js +11 -8
- package/dist/hooks/post-rewrite.js +11 -8
- package/dist/hooks/post-tool-use.js +13 -15
- package/dist/hooks/pre-commit.js +4 -4
- package/dist/hooks/pre-tool-use.js +10 -12
- package/dist/hooks/prim-hook.js +17 -14
- package/dist/hooks/session-end.js +4 -4
- package/dist/hooks/session-start.js +12 -36
- package/dist/index.js +867 -291
- package/dist/statusline-main.js +23 -1
- package/package.json +1 -1
- package/dist/chunk-BTNNEH2W.js +0 -79
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
ensurePostRewriteHookAtPath,
|
|
9
9
|
inspectEffectivePostCommitHook,
|
|
10
10
|
inspectEffectivePostRewriteHook,
|
|
11
|
+
kickDaemonEnsure,
|
|
11
12
|
postCommitHookBlock,
|
|
12
13
|
postRewriteHookBlock,
|
|
13
14
|
printJson,
|
|
@@ -17,42 +18,53 @@ import {
|
|
|
17
18
|
uninstallPostRewriteHookAtPath,
|
|
18
19
|
uninstallProjectPostCommitHook,
|
|
19
20
|
uninstallProjectPostRewriteHook
|
|
20
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-DCJN6LZH.js";
|
|
21
22
|
import {
|
|
22
23
|
requireDurableIngestAcknowledgement
|
|
23
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-TYAHC25F.js";
|
|
24
25
|
import {
|
|
25
26
|
checkAffectedDecisions,
|
|
26
27
|
daemonOrDirectGet,
|
|
27
28
|
formatDecisionsWarning
|
|
28
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-UKUAAAXE.js";
|
|
29
30
|
import {
|
|
30
31
|
fetchFeedbackCapability
|
|
31
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-OH3EZXYX.js";
|
|
33
|
+
import {
|
|
34
|
+
isUnattended,
|
|
35
|
+
unattendedEnv
|
|
36
|
+
} from "./chunk-OYHYCK4K.js";
|
|
32
37
|
import {
|
|
33
38
|
JOURNAL_DIR,
|
|
34
39
|
SESSIONS_DIR,
|
|
35
40
|
bucketStats,
|
|
41
|
+
drainResumeOffset,
|
|
42
|
+
envSlug,
|
|
36
43
|
listBuckets,
|
|
37
44
|
listFlushing,
|
|
38
45
|
pendingJournalStats,
|
|
39
|
-
readMovesFromPath
|
|
40
|
-
|
|
46
|
+
readMovesFromPath,
|
|
47
|
+
removeDrainCheckpoint,
|
|
48
|
+
rotationIdentity,
|
|
49
|
+
sweepOrphanedDrainProgress,
|
|
50
|
+
writeDrainCheckpoint
|
|
51
|
+
} from "./chunk-DXJQR4QI.js";
|
|
41
52
|
import {
|
|
53
|
+
MS_PER_SECOND,
|
|
54
|
+
deliveryBacklogState,
|
|
55
|
+
deliveryBacklogSummary,
|
|
56
|
+
formatPendingBacklog,
|
|
42
57
|
formatStatusline,
|
|
43
58
|
formatTeammates,
|
|
44
59
|
repositoryBindingDiagnosticLabel
|
|
45
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-ZU563PQI.js";
|
|
46
61
|
import {
|
|
47
62
|
bold,
|
|
48
63
|
boundedHealthError,
|
|
49
64
|
color,
|
|
50
65
|
colorForArea,
|
|
51
|
-
dim,
|
|
52
|
-
stripAnsi
|
|
53
|
-
} from "./chunk-BTNNEH2W.js";
|
|
54
|
-
import {
|
|
55
66
|
decisionIngestionStatus,
|
|
67
|
+
dim,
|
|
56
68
|
fetchAndCacheCollectScope,
|
|
57
69
|
isCliErrorResponse,
|
|
58
70
|
isDecisionIdRequest,
|
|
@@ -68,8 +80,9 @@ import {
|
|
|
68
80
|
isValidRepoSyncId,
|
|
69
81
|
repoSyncId,
|
|
70
82
|
repositoryBindingState,
|
|
71
|
-
setRepoActive
|
|
72
|
-
|
|
83
|
+
setRepoActive,
|
|
84
|
+
stripAnsi
|
|
85
|
+
} from "./chunk-DSWT6BCC.js";
|
|
73
86
|
import {
|
|
74
87
|
CliManagementResponseError,
|
|
75
88
|
HttpError,
|
|
@@ -79,13 +92,14 @@ import {
|
|
|
79
92
|
getPinnedClient,
|
|
80
93
|
getPinnedManagementClient,
|
|
81
94
|
getSiteUrl,
|
|
95
|
+
getSiteUrlForEnvironment,
|
|
82
96
|
getTokenExpiresAt,
|
|
83
97
|
isSessionEnded,
|
|
84
98
|
refreshToken,
|
|
85
99
|
setStoredToken,
|
|
86
100
|
stripControlChars,
|
|
87
101
|
terminalSafeLine
|
|
88
|
-
} from "./chunk-
|
|
102
|
+
} from "./chunk-G2VMA2QO.js";
|
|
89
103
|
import {
|
|
90
104
|
assertOwnedHookRuntime,
|
|
91
105
|
atomicWrite,
|
|
@@ -100,6 +114,7 @@ import {
|
|
|
100
114
|
inspectHookRuntimeResolutions,
|
|
101
115
|
inspectHookRuntimeResolutions2,
|
|
102
116
|
makeRegistration,
|
|
117
|
+
packageRoot,
|
|
103
118
|
packageVersion,
|
|
104
119
|
performStatus,
|
|
105
120
|
performStatus2,
|
|
@@ -111,9 +126,10 @@ import {
|
|
|
111
126
|
registerCursorCommands,
|
|
112
127
|
removeHookRuntime,
|
|
113
128
|
stableHookCommand,
|
|
129
|
+
stableNodePath,
|
|
114
130
|
stageHookRuntime,
|
|
115
131
|
stripCommand
|
|
116
|
-
} from "./chunk-
|
|
132
|
+
} from "./chunk-X64HTHUF.js";
|
|
117
133
|
import {
|
|
118
134
|
canonicalGitRoot,
|
|
119
135
|
canonicalRepositoryPath,
|
|
@@ -122,7 +138,7 @@ import {
|
|
|
122
138
|
import {
|
|
123
139
|
daemonIsLive,
|
|
124
140
|
daemonRequest
|
|
125
|
-
} from "./chunk-
|
|
141
|
+
} from "./chunk-OTHKG76J.js";
|
|
126
142
|
import {
|
|
127
143
|
REFRESH_TOKEN_PATH,
|
|
128
144
|
TOKEN_FILE_PATH,
|
|
@@ -135,12 +151,13 @@ import {
|
|
|
135
151
|
requestWorkosDeviceAuthorization,
|
|
136
152
|
resolveAuthCredential,
|
|
137
153
|
syncDirectory,
|
|
138
|
-
withFileLock
|
|
139
|
-
|
|
154
|
+
withFileLock,
|
|
155
|
+
withFileLockSync
|
|
156
|
+
} from "./chunk-2GP5UFV7.js";
|
|
140
157
|
|
|
141
158
|
// src/index.ts
|
|
142
|
-
import { readFileSync as
|
|
143
|
-
import { dirname as
|
|
159
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
160
|
+
import { dirname as dirname7, resolve as resolve4 } from "path";
|
|
144
161
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
145
162
|
import { Command } from "commander";
|
|
146
163
|
import updateNotifier from "update-notifier";
|
|
@@ -749,18 +766,18 @@ function parseAuthBrokerConfig(value) {
|
|
|
749
766
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
750
767
|
throw new Error("broker config must be an object");
|
|
751
768
|
}
|
|
752
|
-
const
|
|
753
|
-
if (typeof
|
|
769
|
+
const record3 = value;
|
|
770
|
+
if (typeof record3.authorization_server !== "string" || !record3.authorization_server.trim()) {
|
|
754
771
|
throw new Error("authorization_server is required");
|
|
755
772
|
}
|
|
756
|
-
if (typeof
|
|
773
|
+
if (typeof record3.client_id !== "string" || !record3.client_id.trim()) {
|
|
757
774
|
throw new Error("client_id is required");
|
|
758
775
|
}
|
|
759
|
-
if (!Array.isArray(
|
|
776
|
+
if (!Array.isArray(record3.default_scopes) || !record3.default_scopes.every((scope) => typeof scope === "string" && scope.length > 0)) {
|
|
760
777
|
throw new Error("default_scopes must be a string array");
|
|
761
778
|
}
|
|
762
|
-
const authorizationServer = parseHttpUrl(
|
|
763
|
-
const rawEndpoint =
|
|
779
|
+
const authorizationServer = parseHttpUrl(record3.authorization_server, "authorization_server");
|
|
780
|
+
const rawEndpoint = record3.authorization_endpoint === void 0 ? DEFAULT_WORKOS_AUTHORIZE_URL : record3.authorization_endpoint;
|
|
764
781
|
if (typeof rawEndpoint !== "string" || !rawEndpoint.trim()) {
|
|
765
782
|
throw new Error("authorization_endpoint must be a URL");
|
|
766
783
|
}
|
|
@@ -771,8 +788,8 @@ function parseAuthBrokerConfig(value) {
|
|
|
771
788
|
return {
|
|
772
789
|
authorization_server: authorizationServer.toString(),
|
|
773
790
|
authorization_endpoint: authorizationEndpoint.toString(),
|
|
774
|
-
client_id:
|
|
775
|
-
default_scopes: [...
|
|
791
|
+
client_id: record3.client_id.trim(),
|
|
792
|
+
default_scopes: [...record3.default_scopes]
|
|
776
793
|
};
|
|
777
794
|
}
|
|
778
795
|
async function fetchAuthBrokerConfig(siteUrl, fetchImpl = fetch) {
|
|
@@ -2177,7 +2194,7 @@ ${line("project", result.project)}`);
|
|
|
2177
2194
|
|
|
2178
2195
|
// src/commands/daemon.ts
|
|
2179
2196
|
import { spawn as spawn2 } from "child_process";
|
|
2180
|
-
import { closeSync, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
|
|
2197
|
+
import { closeSync as closeSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
|
|
2181
2198
|
import { join as join5 } from "path";
|
|
2182
2199
|
import { Option as Option2 } from "commander";
|
|
2183
2200
|
|
|
@@ -2250,12 +2267,12 @@ async function runLatestDaemonBootstrap(ensureCurrent, options = {}) {
|
|
|
2250
2267
|
import { spawnSync } from "child_process";
|
|
2251
2268
|
import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
|
|
2252
2269
|
import {
|
|
2253
|
-
constants,
|
|
2270
|
+
constants as constants2,
|
|
2254
2271
|
accessSync,
|
|
2255
2272
|
chmodSync,
|
|
2256
2273
|
copyFileSync,
|
|
2257
2274
|
existsSync as existsSync3,
|
|
2258
|
-
lstatSync as
|
|
2275
|
+
lstatSync as lstatSync3,
|
|
2259
2276
|
mkdirSync as mkdirSync2,
|
|
2260
2277
|
mkdtempSync,
|
|
2261
2278
|
readFileSync as readFileSync3,
|
|
@@ -2264,16 +2281,53 @@ import {
|
|
|
2264
2281
|
realpathSync,
|
|
2265
2282
|
renameSync,
|
|
2266
2283
|
rmSync,
|
|
2284
|
+
statSync,
|
|
2267
2285
|
symlinkSync,
|
|
2268
2286
|
writeFileSync as writeFileSync2
|
|
2269
2287
|
} from "fs";
|
|
2270
2288
|
import { homedir as homedir3 } from "os";
|
|
2271
2289
|
import { basename, dirname as dirname2, isAbsolute, join as join4, resolve as resolve2 } from "path";
|
|
2272
2290
|
|
|
2291
|
+
// src/lib/bounded-file.ts
|
|
2292
|
+
import { constants, closeSync, fstatSync, lstatSync as lstatSync2, openSync, readSync } from "fs";
|
|
2293
|
+
function readBoundedRegularFile(path, maxBytes) {
|
|
2294
|
+
let fd;
|
|
2295
|
+
try {
|
|
2296
|
+
const linked = lstatSync2(path);
|
|
2297
|
+
if (!linked.isFile() || linked.size > maxBytes) return void 0;
|
|
2298
|
+
fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
2299
|
+
const opened = fstatSync(fd);
|
|
2300
|
+
if (!opened.isFile() || opened.dev !== linked.dev || opened.ino !== linked.ino || opened.size > maxBytes) {
|
|
2301
|
+
return void 0;
|
|
2302
|
+
}
|
|
2303
|
+
const buffer = Buffer.allocUnsafe(opened.size + 1);
|
|
2304
|
+
let length = 0;
|
|
2305
|
+
while (length < buffer.length) {
|
|
2306
|
+
const count = readSync(fd, buffer, length, buffer.length - length, length);
|
|
2307
|
+
if (count === 0) break;
|
|
2308
|
+
length += count;
|
|
2309
|
+
}
|
|
2310
|
+
if (length > opened.size) return void 0;
|
|
2311
|
+
return { text: buffer.subarray(0, length).toString("utf8"), ownerUid: opened.uid };
|
|
2312
|
+
} catch {
|
|
2313
|
+
return void 0;
|
|
2314
|
+
} finally {
|
|
2315
|
+
if (fd !== void 0) {
|
|
2316
|
+
try {
|
|
2317
|
+
closeSync(fd);
|
|
2318
|
+
} catch {
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2273
2324
|
// src/daemon/env-binding.ts
|
|
2274
2325
|
function normalizeApiUrl(url) {
|
|
2275
2326
|
return url.trim().replace(/\/+$/, "");
|
|
2276
2327
|
}
|
|
2328
|
+
function apiUrlsMatch(a, b) {
|
|
2329
|
+
return normalizeApiUrl(a) === normalizeApiUrl(b);
|
|
2330
|
+
}
|
|
2277
2331
|
|
|
2278
2332
|
// src/daemon/launchd.ts
|
|
2279
2333
|
var LAUNCHD_LABEL = "ai.getprimitive.prim-daemon";
|
|
@@ -2292,6 +2346,11 @@ var SERVICE_NOT_FOUND = 113;
|
|
|
2292
2346
|
var TRANSITION_IN_PROGRESS = 5;
|
|
2293
2347
|
var OUTPUT_LIMIT = 4096;
|
|
2294
2348
|
var DAEMON_DISABLED_CONTENT = "disabled by `prim daemon stop`\n";
|
|
2349
|
+
var LAUNCHER_MAX_BYTES = 64 * 1024;
|
|
2350
|
+
var PLIST_MAX_BYTES = 64 * 1024;
|
|
2351
|
+
var DRIFT_HEAL_MARKER = "daemon-drift-heal.json";
|
|
2352
|
+
var DRIFT_HEAL_MARKER_TEMPORARY_RE = /^daemon-drift-heal\.json\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.tmp$/u;
|
|
2353
|
+
var DRIFT_HEAL_CLAIM_OWNER = "owner.json";
|
|
2295
2354
|
var LaunchctlError = class extends Error {
|
|
2296
2355
|
status;
|
|
2297
2356
|
timedOut;
|
|
@@ -2331,6 +2390,8 @@ function daemonControlPaths(options = {}) {
|
|
|
2331
2390
|
return {
|
|
2332
2391
|
launcher: join4(configDir, `prim-daemon-launcher-v${LAUNCHER_SCHEMA_VERSION}`),
|
|
2333
2392
|
disabledMarker: join4(configDir, "daemon.disabled"),
|
|
2393
|
+
driftHealMarker: join4(configDir, DRIFT_HEAL_MARKER),
|
|
2394
|
+
driftHealClaim: join4(configDir, "daemon-drift-heal.lock"),
|
|
2334
2395
|
lifecycleLockDir: join4(configDir, "daemon.lifecycle.lock"),
|
|
2335
2396
|
legacyDisabledMarker: legacy.disabledMarker,
|
|
2336
2397
|
legacyLifecycleLockDir: legacy.lifecycleLockDir
|
|
@@ -2391,7 +2452,7 @@ function readOwnedRuntimeManifest(releaseDir) {
|
|
|
2391
2452
|
}
|
|
2392
2453
|
const manifest = parsed;
|
|
2393
2454
|
if (manifest.schemaVersion !== RUNTIME_SCHEMA_VERSION || compareSemver(manifest.version, manifest.version) !== 0 || !isAbsolute(manifest.nodePath) || /[\r\n]/u.test(manifest.nodePath) || manifest.daemonFile !== "prim-daemon-server" || !/^[0-9a-f]{64}$/u.test(manifest.daemonSha256) || new Date(manifest.stagedAt).toISOString() !== manifest.stagedAt || raw !== `${JSON.stringify(manifest, null, 2)}
|
|
2394
|
-
` || !
|
|
2455
|
+
` || !lstatSync3(daemonPath).isFile() || sha256File(daemonPath) !== manifest.daemonSha256) {
|
|
2395
2456
|
return null;
|
|
2396
2457
|
}
|
|
2397
2458
|
const entries = readdirSync2(releaseDir).sort();
|
|
@@ -2400,8 +2461,17 @@ function readOwnedRuntimeManifest(releaseDir) {
|
|
|
2400
2461
|
return null;
|
|
2401
2462
|
}
|
|
2402
2463
|
}
|
|
2464
|
+
function sameNode(recorded, desired) {
|
|
2465
|
+
if (recorded === desired) return true;
|
|
2466
|
+
try {
|
|
2467
|
+
const target = realpathSync(desired);
|
|
2468
|
+
return recorded !== target && realpathSync(recorded) === target;
|
|
2469
|
+
} catch {
|
|
2470
|
+
return false;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2403
2473
|
function sameRuntime(current, desired, daemonPath) {
|
|
2404
|
-
if (!current || current.version !== desired.version || current.nodePath
|
|
2474
|
+
if (!current || current.version !== desired.version || !sameNode(current.nodePath, desired.nodePath) || current.daemonSha256 !== desired.daemonSha256) {
|
|
2405
2475
|
return false;
|
|
2406
2476
|
}
|
|
2407
2477
|
try {
|
|
@@ -2429,7 +2499,7 @@ function stageRuntime(options = {}) {
|
|
|
2429
2499
|
if (!daemonSource || !existsSync3(daemonSource)) {
|
|
2430
2500
|
throw new Error("cannot stage runtime: prim-daemon-server bundle is unavailable");
|
|
2431
2501
|
}
|
|
2432
|
-
const nodePath = resolve2(options.nodePath ??
|
|
2502
|
+
const nodePath = resolve2(options.nodePath ?? stableNodePath());
|
|
2433
2503
|
const version = options.version ?? findPackageVersion(daemonSource);
|
|
2434
2504
|
const desired = {
|
|
2435
2505
|
schemaVersion: RUNTIME_SCHEMA_VERSION,
|
|
@@ -2528,13 +2598,30 @@ function assertRegularFile(path, label) {
|
|
|
2528
2598
|
}
|
|
2529
2599
|
function lstatIfPresent2(path) {
|
|
2530
2600
|
try {
|
|
2531
|
-
return
|
|
2601
|
+
return lstatSync3(path);
|
|
2532
2602
|
} catch (error) {
|
|
2533
2603
|
const code = error.code;
|
|
2534
2604
|
if (code === "ENOENT" || code === "ENOTDIR") return void 0;
|
|
2535
2605
|
throw error;
|
|
2536
2606
|
}
|
|
2537
2607
|
}
|
|
2608
|
+
function driftHealMarkerTemporaries(configDir) {
|
|
2609
|
+
try {
|
|
2610
|
+
return readdirSync2(configDir).filter((entry) => DRIFT_HEAL_MARKER_TEMPORARY_RE.test(entry)).map((entry) => join4(configDir, entry));
|
|
2611
|
+
} catch (error) {
|
|
2612
|
+
const code = error.code;
|
|
2613
|
+
if (code === "ENOENT" || code === "ENOTDIR") return [];
|
|
2614
|
+
throw error;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
function assertDriftHealClaim(path) {
|
|
2618
|
+
const claim = lstatIfPresent2(path);
|
|
2619
|
+
if (!claim) return;
|
|
2620
|
+
if (!claim.isDirectory() || readdirSync2(path).some((entry) => entry !== DRIFT_HEAL_CLAIM_OWNER)) {
|
|
2621
|
+
throw new Error(`refusing to remove unrecognized daemon drift-heal claim at ${path}`);
|
|
2622
|
+
}
|
|
2623
|
+
assertRegularFile(join4(path, DRIFT_HEAL_CLAIM_OWNER), "daemon drift-heal claim owner");
|
|
2624
|
+
}
|
|
2538
2625
|
function assertExactFile(path, expected, label) {
|
|
2539
2626
|
if (!lstatIfPresent2(path)) return;
|
|
2540
2627
|
assertRegularFile(path, label);
|
|
@@ -2573,17 +2660,16 @@ function assertOwnedDaemonRuntime(options) {
|
|
|
2573
2660
|
);
|
|
2574
2661
|
}
|
|
2575
2662
|
assertRegularFile(logPath, "daemon log");
|
|
2663
|
+
assertRegularFile(control.driftHealMarker, "daemon drift-heal marker");
|
|
2664
|
+
for (const temporary of driftHealMarkerTemporaries(configDir)) {
|
|
2665
|
+
assertRegularFile(temporary, "daemon drift-heal marker temporary");
|
|
2666
|
+
}
|
|
2667
|
+
assertDriftHealClaim(control.driftHealClaim);
|
|
2576
2668
|
assertRegularFile(join4(configDir, "daemon-health.json"), "daemon health snapshot");
|
|
2577
2669
|
assertRegularFile(join4(configDir, "client_instance_id"), "daemon client instance id");
|
|
2578
2670
|
if (service && lstatIfPresent2(service.plistPath)) {
|
|
2579
2671
|
assertRegularFile(service.plistPath, "launchd property list");
|
|
2580
|
-
|
|
2581
|
-
launcherPath: control.launcher,
|
|
2582
|
-
logPath,
|
|
2583
|
-
workingDirectory: options.homeDir ?? homedir3(),
|
|
2584
|
-
label: options.label
|
|
2585
|
-
});
|
|
2586
|
-
if (readFileSync3(service.plistPath, "utf8") !== expected) {
|
|
2672
|
+
if (readFileSync3(service.plistPath, "utf8") !== ownedLaunchAgentPlist(options, logPath)) {
|
|
2587
2673
|
throw new Error(
|
|
2588
2674
|
`refusing to remove unrecognized launchd property list at ${service.plistPath}`
|
|
2589
2675
|
);
|
|
@@ -2660,6 +2746,9 @@ async function removeDaemonRuntime(options = {}) {
|
|
|
2660
2746
|
control.launcher,
|
|
2661
2747
|
control.disabledMarker,
|
|
2662
2748
|
control.legacyDisabledMarker,
|
|
2749
|
+
control.driftHealMarker,
|
|
2750
|
+
...driftHealMarkerTemporaries(configDir),
|
|
2751
|
+
control.driftHealClaim,
|
|
2663
2752
|
...service ? [service.plistPath] : [],
|
|
2664
2753
|
logPath,
|
|
2665
2754
|
join4(configDir, "daemon-health.json"),
|
|
@@ -2671,7 +2760,7 @@ async function removeDaemonRuntime(options = {}) {
|
|
|
2671
2760
|
return withDaemonLifecycleLock(async () => {
|
|
2672
2761
|
assertOwnedDaemonRuntime(options);
|
|
2673
2762
|
for (const path of ownedArtifacts.filter((candidate) => candidate !== runtime.runtimeDir)) {
|
|
2674
|
-
rmSync(path, { force: true });
|
|
2763
|
+
rmSync(path, { force: true, recursive: path === control.driftHealClaim });
|
|
2675
2764
|
}
|
|
2676
2765
|
if (lstatIfPresent2(runtime.runtimeDir)) {
|
|
2677
2766
|
const quarantined = `${runtime.runtimeDir}.uninstall-${String(process.pid)}-${randomBytes3(8).toString("hex")}`;
|
|
@@ -2705,9 +2794,22 @@ ${config.configDir ? `export PRIM_CONFIG_DIR=${shellQuote(config.configDir)}
|
|
|
2705
2794
|
`
|
|
2706
2795
|
};
|
|
2707
2796
|
}
|
|
2797
|
+
function ownedLaunchAgentPlist(options, logPath) {
|
|
2798
|
+
return generateLaunchAgentPlist({
|
|
2799
|
+
launcherPath: daemonControlPaths(options).launcher,
|
|
2800
|
+
logPath,
|
|
2801
|
+
workingDirectory: options.homeDir ?? homedir3(),
|
|
2802
|
+
label: options.label
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2708
2805
|
function readDaemonLauncher(path) {
|
|
2806
|
+
return readDaemonLauncherFile(path)?.config ?? null;
|
|
2807
|
+
}
|
|
2808
|
+
function readDaemonLauncherFile(path) {
|
|
2709
2809
|
try {
|
|
2710
|
-
const
|
|
2810
|
+
const file = readBoundedRegularFile(path, LAUNCHER_MAX_BYTES);
|
|
2811
|
+
if (!file) return null;
|
|
2812
|
+
const content = file.text;
|
|
2711
2813
|
const encoded = /^# prim-daemon-launcher: ([A-Za-z0-9_-]+)$/mu.exec(content)?.[1];
|
|
2712
2814
|
if (!encoded) return null;
|
|
2713
2815
|
const value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
@@ -2722,7 +2824,7 @@ function readDaemonLauncher(path) {
|
|
|
2722
2824
|
...typeof value.configDir === "string" ? { configDir: value.configDir } : {}
|
|
2723
2825
|
};
|
|
2724
2826
|
const expected = generateDaemonLauncher(config);
|
|
2725
|
-
return expected.revision === value.revision && expected.content === content ? config : null;
|
|
2827
|
+
return expected.revision === value.revision && expected.content === content ? { config, ownerUid: file.ownerUid } : null;
|
|
2726
2828
|
} catch {
|
|
2727
2829
|
return null;
|
|
2728
2830
|
}
|
|
@@ -2732,7 +2834,7 @@ function readUsableDaemonConfig(path) {
|
|
|
2732
2834
|
if (!config) return null;
|
|
2733
2835
|
const manifest = readRuntimeManifest(join4(dirname2(config.daemonPath), "manifest.json"));
|
|
2734
2836
|
try {
|
|
2735
|
-
accessSync(config.nodePath,
|
|
2837
|
+
accessSync(config.nodePath, constants2.X_OK);
|
|
2736
2838
|
return manifest?.version === config.runtimeVersion && manifest.nodePath === config.nodePath && manifest.daemonSha256 === sha256File(config.daemonPath) ? config : null;
|
|
2737
2839
|
} catch {
|
|
2738
2840
|
return null;
|
|
@@ -3076,6 +3178,68 @@ function setDaemonExplicitlyDisabled(disabled, options = {}) {
|
|
|
3076
3178
|
}
|
|
3077
3179
|
atomicWrite2(paths.disabledMarker, content, RUNTIME_FILE_MODE);
|
|
3078
3180
|
}
|
|
3181
|
+
function launcherRunnable(config) {
|
|
3182
|
+
try {
|
|
3183
|
+
accessSync(config.nodePath, constants2.X_OK);
|
|
3184
|
+
return statSync(config.daemonPath).isFile();
|
|
3185
|
+
} catch {
|
|
3186
|
+
return false;
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
function selectedDaemonLauncher(options = {}) {
|
|
3190
|
+
const file = readDaemonLauncherFile(daemonControlPaths(options).launcher);
|
|
3191
|
+
if (!file) return null;
|
|
3192
|
+
const { config, ownerUid } = file;
|
|
3193
|
+
return {
|
|
3194
|
+
runtimeVersion: config.runtimeVersion,
|
|
3195
|
+
...config.apiUrl ? { apiUrl: config.apiUrl } : {},
|
|
3196
|
+
ownerUid,
|
|
3197
|
+
runnable: launcherRunnable(config)
|
|
3198
|
+
};
|
|
3199
|
+
}
|
|
3200
|
+
var XML_ENTITIES = {
|
|
3201
|
+
"&": "&",
|
|
3202
|
+
"<": "<",
|
|
3203
|
+
">": ">",
|
|
3204
|
+
""": '"',
|
|
3205
|
+
"'": "'"
|
|
3206
|
+
};
|
|
3207
|
+
var XML_ENTITY_RE = /&(?:amp|lt|gt|quot|apos);/gu;
|
|
3208
|
+
var PLIST_LABEL_KEY = "<key>Label</key>";
|
|
3209
|
+
var PLIST_PROGRAM_KEY = "<key>ProgramArguments</key>";
|
|
3210
|
+
var PLIST_LABEL_RE = /<key>Label<\/key>\s*<string>([^<]*)<\/string>/u;
|
|
3211
|
+
var PLIST_PROGRAM_RE = /<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]*)<\/string>/u;
|
|
3212
|
+
function xmlUnescape(value) {
|
|
3213
|
+
if (value.replace(XML_ENTITY_RE, "").includes("&")) return void 0;
|
|
3214
|
+
return value.replace(XML_ENTITY_RE, (entity) => XML_ENTITIES[entity] ?? entity);
|
|
3215
|
+
}
|
|
3216
|
+
function launchAgentProgram(plist) {
|
|
3217
|
+
if (plist.split(PLIST_LABEL_KEY).length !== 2 || plist.split(PLIST_PROGRAM_KEY).length !== 2) {
|
|
3218
|
+
return void 0;
|
|
3219
|
+
}
|
|
3220
|
+
const label = PLIST_LABEL_RE.exec(plist)?.[1];
|
|
3221
|
+
const program2 = PLIST_PROGRAM_RE.exec(plist)?.[1];
|
|
3222
|
+
if (label === void 0 || program2 === void 0) return void 0;
|
|
3223
|
+
const decodedLabel = xmlUnescape(label);
|
|
3224
|
+
const decodedProgram = xmlUnescape(program2);
|
|
3225
|
+
return decodedLabel === void 0 || decodedProgram === void 0 ? void 0 : { label: decodedLabel, program: decodedProgram };
|
|
3226
|
+
}
|
|
3227
|
+
function launchAgentRunsConfigRoot(options = {}) {
|
|
3228
|
+
try {
|
|
3229
|
+
const service = launchdPaths(options);
|
|
3230
|
+
const plist = readBoundedRegularFile(service.plistPath, PLIST_MAX_BYTES);
|
|
3231
|
+
const agent = plist ? launchAgentProgram(plist.text) : void 0;
|
|
3232
|
+
return agent?.label === (options.label ?? LAUNCHD_LABEL) && agent.program === daemonControlPaths(options).launcher;
|
|
3233
|
+
} catch {
|
|
3234
|
+
return false;
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
function daemonDriftHealMarkerPath(options = {}) {
|
|
3238
|
+
return daemonControlPaths(options).driftHealMarker;
|
|
3239
|
+
}
|
|
3240
|
+
function daemonDriftHealClaimPath(options = {}) {
|
|
3241
|
+
return daemonControlPaths(options).driftHealClaim;
|
|
3242
|
+
}
|
|
3079
3243
|
async function ensureMacDaemon(options = {}) {
|
|
3080
3244
|
return withDaemonLifecycleLock(() => ensureMacDaemonLocked(options), options);
|
|
3081
3245
|
}
|
|
@@ -3277,16 +3441,20 @@ var STATUS_PROBE_TIMEOUT_MS = 500;
|
|
|
3277
3441
|
var READY_TIMEOUT_MS = 5e3;
|
|
3278
3442
|
var READY_POLL_MS2 = 100;
|
|
3279
3443
|
var READY_PROBE_TIMEOUT_MS2 = 250;
|
|
3280
|
-
var
|
|
3281
|
-
var
|
|
3444
|
+
var READY_SNAPSHOT_TIMEOUT_MS = 3e4;
|
|
3445
|
+
var READY_SNAPSHOT_POLL_MS = 250;
|
|
3282
3446
|
var EXIT_OK3 = 0;
|
|
3283
3447
|
var EXIT_NOT_RUNNING = 2;
|
|
3284
3448
|
var EXIT_BOOTING = 3;
|
|
3285
|
-
|
|
3286
|
-
|
|
3449
|
+
var VERIFIED_PREFIX = "[prim] \u2713 ";
|
|
3450
|
+
function formatDaemonLifecycleMessage(message, decisionIngestion, snapshot, now = Date.now()) {
|
|
3451
|
+
const backlog = deliveryBacklogSummary(snapshot?.ingestion, now);
|
|
3452
|
+
const state = deliveryBacklogState(snapshot?.ingestion);
|
|
3453
|
+
const verdict = (state === "failing" || state === "retained") && message.startsWith(VERIFIED_PREFIX) ? `[prim] \u26A0 ${message.slice(VERIFIED_PREFIX.length)}` : message;
|
|
3454
|
+
return `${verdict} \xB7 Decision ingestion ${decisionIngestion}${backlog ? ` \xB7 ${backlog}` : ""}`;
|
|
3287
3455
|
}
|
|
3288
|
-
function formatCurrentDaemonLifecycleMessage(message) {
|
|
3289
|
-
return formatDaemonLifecycleMessage(message, decisionIngestionStatus(process.cwd()));
|
|
3456
|
+
function formatCurrentDaemonLifecycleMessage(message, snapshot) {
|
|
3457
|
+
return formatDaemonLifecycleMessage(message, decisionIngestionStatus(process.cwd()), snapshot);
|
|
3290
3458
|
}
|
|
3291
3459
|
function daemonDegradedReason(snapshot) {
|
|
3292
3460
|
if (!snapshot || snapshot.healthy !== false) return void 0;
|
|
@@ -3337,7 +3505,7 @@ function openDaemonLog(configDir = CONFIG_DIR) {
|
|
|
3337
3505
|
if (!existsSync4(configDir)) {
|
|
3338
3506
|
mkdirSync3(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
3339
3507
|
}
|
|
3340
|
-
return
|
|
3508
|
+
return openSync2(join5(configDir, "daemon.log"), "a", LOG_FILE_MODE);
|
|
3341
3509
|
}
|
|
3342
3510
|
async function waitForReady() {
|
|
3343
3511
|
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
@@ -3349,17 +3517,23 @@ async function waitForReady() {
|
|
|
3349
3517
|
}
|
|
3350
3518
|
return daemonIsLive(READY_PROBE_TIMEOUT_MS2);
|
|
3351
3519
|
}
|
|
3352
|
-
|
|
3353
|
-
|
|
3520
|
+
function requestStatusSnapshot() {
|
|
3521
|
+
return daemonRequest(
|
|
3522
|
+
"status_snapshot",
|
|
3523
|
+
{},
|
|
3524
|
+
{ timeoutMs: STATUS_PROBE_TIMEOUT_MS }
|
|
3525
|
+
);
|
|
3526
|
+
}
|
|
3527
|
+
async function waitForReadySnapshot(expectedVersion, options = {}) {
|
|
3528
|
+
const request = options.requestSnapshot ?? requestStatusSnapshot;
|
|
3529
|
+
const wait = options.sleep ?? sleep;
|
|
3530
|
+
const nowMs = options.nowMs ?? Date.now;
|
|
3531
|
+
const deadline = nowMs() + READY_SNAPSHOT_TIMEOUT_MS;
|
|
3354
3532
|
let snapshot = null;
|
|
3355
|
-
while (
|
|
3356
|
-
snapshot = await
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
{ timeoutMs: STATUS_PROBE_TIMEOUT_MS }
|
|
3360
|
-
);
|
|
3361
|
-
if (snapshot?.healthy === true && snapshot.version === expectedVersion) return snapshot;
|
|
3362
|
-
await sleep(HEALTHY_POLL_MS);
|
|
3533
|
+
while (nowMs() < deadline) {
|
|
3534
|
+
snapshot = await request();
|
|
3535
|
+
if (daemonStartIsReady(true, snapshot, expectedVersion)) return snapshot;
|
|
3536
|
+
await wait(READY_SNAPSHOT_POLL_MS);
|
|
3363
3537
|
}
|
|
3364
3538
|
return snapshot;
|
|
3365
3539
|
}
|
|
@@ -3372,11 +3546,20 @@ async function verifiedPid(existing) {
|
|
|
3372
3546
|
);
|
|
3373
3547
|
return snapshot?.pid === existing.pid ? snapshot : null;
|
|
3374
3548
|
}
|
|
3375
|
-
function
|
|
3376
|
-
|
|
3549
|
+
function daemonStartIsReady(serviceReady, snapshot, expectedVersion) {
|
|
3550
|
+
if (!serviceReady || !snapshot || expectedVersion === void 0) return false;
|
|
3551
|
+
if (snapshot.version !== expectedVersion || snapshot.needsReauth === true) return false;
|
|
3552
|
+
if (snapshot.healthy === true) return true;
|
|
3553
|
+
return snapshot.heartbeat?.healthy === true && snapshot.ingestion?.healthy === false;
|
|
3554
|
+
}
|
|
3555
|
+
function daemonBacklogFields(snapshot) {
|
|
3556
|
+
const state = deliveryBacklogState(snapshot?.ingestion);
|
|
3557
|
+
if (!state) return {};
|
|
3558
|
+
if (state === "draining") return { draining: true, ingestion: snapshot?.ingestion };
|
|
3559
|
+
return state === "retained" ? { draining: false, deliveryRetained: true, ingestion: snapshot?.ingestion } : { draining: false, deliveryFailing: true, ingestion: snapshot?.ingestion };
|
|
3377
3560
|
}
|
|
3378
|
-
function daemonStartHealthFields(
|
|
3379
|
-
if (
|
|
3561
|
+
function daemonStartHealthFields(ready, snapshot) {
|
|
3562
|
+
if (ready) return daemonBacklogFields(snapshot);
|
|
3380
3563
|
return {
|
|
3381
3564
|
state: "degraded",
|
|
3382
3565
|
needsReauth: snapshot?.needsReauth === true,
|
|
@@ -3384,16 +3567,28 @@ function daemonStartHealthFields(healthy, snapshot) {
|
|
|
3384
3567
|
ingestion: snapshot?.ingestion ?? null
|
|
3385
3568
|
};
|
|
3386
3569
|
}
|
|
3570
|
+
function daemonStartFailureReason(snapshot, expectedVersion) {
|
|
3571
|
+
if (snapshot && !snapshot.needsReauth && expectedVersion !== void 0 && snapshot.version !== expectedVersion) {
|
|
3572
|
+
return `daemon reports ${snapshot.version ? `v${snapshot.version}` : "no version"}, expected v${expectedVersion}`;
|
|
3573
|
+
}
|
|
3574
|
+
return daemonDegradedReason(snapshot);
|
|
3575
|
+
}
|
|
3387
3576
|
async function detachedDaemonStart(opts) {
|
|
3388
3577
|
const existing = readPidfile();
|
|
3389
3578
|
if (existing?.alive) {
|
|
3390
3579
|
const snapshot = await verifiedPid(existing);
|
|
3391
3580
|
if (snapshot) {
|
|
3392
3581
|
process.stderr.write(
|
|
3393
|
-
`${formatCurrentDaemonLifecycleMessage(`[prim] daemon already running (pid=${existing.pid})
|
|
3582
|
+
`${formatCurrentDaemonLifecycleMessage(`[prim] daemon already running (pid=${existing.pid})`, snapshot)}
|
|
3394
3583
|
`
|
|
3395
3584
|
);
|
|
3396
|
-
console.log(
|
|
3585
|
+
console.log(
|
|
3586
|
+
JSON.stringify(
|
|
3587
|
+
{ started: false, pid: existing.pid, ...daemonBacklogFields(snapshot) },
|
|
3588
|
+
null,
|
|
3589
|
+
2
|
|
3590
|
+
)
|
|
3591
|
+
);
|
|
3397
3592
|
return;
|
|
3398
3593
|
}
|
|
3399
3594
|
process.stderr.write(
|
|
@@ -3442,16 +3637,19 @@ async function detachedDaemonStart(opts) {
|
|
|
3442
3637
|
});
|
|
3443
3638
|
child.unref();
|
|
3444
3639
|
if (logFd !== void 0) {
|
|
3445
|
-
|
|
3640
|
+
closeSync2(logFd);
|
|
3446
3641
|
}
|
|
3447
3642
|
const live = await waitForReady();
|
|
3448
3643
|
if (live) {
|
|
3449
3644
|
const after = readPidfile();
|
|
3645
|
+
const snapshot = await requestStatusSnapshot();
|
|
3450
3646
|
process.stderr.write(
|
|
3451
|
-
`${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon started (pid=${after?.pid ?? "?"}, socket=${SOCK_PATH})
|
|
3647
|
+
`${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon started (pid=${after?.pid ?? "?"}, socket=${SOCK_PATH})`, snapshot)}
|
|
3452
3648
|
`
|
|
3453
3649
|
);
|
|
3454
|
-
console.log(
|
|
3650
|
+
console.log(
|
|
3651
|
+
JSON.stringify({ started: true, pid: after?.pid, ...daemonBacklogFields(snapshot) }, null, 2)
|
|
3652
|
+
);
|
|
3455
3653
|
return;
|
|
3456
3654
|
}
|
|
3457
3655
|
process.stderr.write(
|
|
@@ -3574,19 +3772,25 @@ async function macDaemonStart(forceRestart = false) {
|
|
|
3574
3772
|
const result = await ensureMacDaemon({ explicitlyStarted: true, forceRestart });
|
|
3575
3773
|
const serviceReady = result.state === "running";
|
|
3576
3774
|
const expectedVersion = result.runtime?.manifest.version;
|
|
3577
|
-
const snapshot = serviceReady && expectedVersion ? await
|
|
3578
|
-
const
|
|
3579
|
-
if (
|
|
3775
|
+
const snapshot = serviceReady && expectedVersion ? await waitForReadySnapshot(expectedVersion) : null;
|
|
3776
|
+
const ready = daemonStartIsReady(serviceReady, snapshot, expectedVersion);
|
|
3777
|
+
if (ready) {
|
|
3580
3778
|
const verb = result.action === "none" ? "already running" : "started";
|
|
3581
3779
|
const servicePid = result.service.loaded ? result.service.pid : void 0;
|
|
3582
3780
|
process.stderr.write(
|
|
3583
|
-
`${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon ${verb} under launchd (pid=${snapshot?.pid ?? servicePid ?? "?"})
|
|
3781
|
+
`${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon ${verb} under launchd (pid=${snapshot?.pid ?? servicePid ?? "?"})`, snapshot)}
|
|
3584
3782
|
`
|
|
3585
3783
|
);
|
|
3784
|
+
} else if (!(serviceReady && expectedVersion)) {
|
|
3785
|
+
process.stderr.write(
|
|
3786
|
+
`[prim] \u2717 launchd did not converge on the desired daemon (${result.action}) (see ${LOG_PATH})
|
|
3787
|
+
`
|
|
3788
|
+
);
|
|
3789
|
+
if (!process.exitCode) process.exitCode = EXIT_NOT_RUNNING;
|
|
3586
3790
|
} else {
|
|
3587
|
-
const reason =
|
|
3791
|
+
const reason = daemonStartFailureReason(snapshot, expectedVersion);
|
|
3588
3792
|
process.stderr.write(
|
|
3589
|
-
`[prim] \u2717 launchd daemon did not reach healthy heartbeat
|
|
3793
|
+
`[prim] \u2717 launchd daemon did not reach a healthy heartbeat on the expected version${reason ? ` \xB7 ${reason}` : ""} (see ${LOG_PATH})
|
|
3590
3794
|
`
|
|
3591
3795
|
);
|
|
3592
3796
|
if (!process.exitCode) process.exitCode = EXIT_NOT_RUNNING;
|
|
@@ -3594,14 +3798,20 @@ async function macDaemonStart(forceRestart = false) {
|
|
|
3594
3798
|
console.log(
|
|
3595
3799
|
JSON.stringify(
|
|
3596
3800
|
{
|
|
3597
|
-
started:
|
|
3801
|
+
started: ready,
|
|
3598
3802
|
supervised: true,
|
|
3599
3803
|
action: result.action,
|
|
3600
3804
|
pid: snapshot?.pid ?? (result.service.loaded ? result.service.pid : void 0),
|
|
3601
3805
|
loaded: result.service.loaded,
|
|
3602
3806
|
responding: result.responding,
|
|
3603
|
-
healthy
|
|
3604
|
-
|
|
3807
|
+
// `healthy` keeps meaning "start succeeded" (always equal to `started`
|
|
3808
|
+
// and the exit code) so existing consumers stay correct. It is not the
|
|
3809
|
+
// daemon's own health: `daemon status --json` stays degraded while a
|
|
3810
|
+
// backlog drains, is held back, or fails to deliver, which start
|
|
3811
|
+
// reports additively via `draining`, `deliveryRetained`,
|
|
3812
|
+
// `deliveryFailing`, and `ingestion`.
|
|
3813
|
+
healthy: ready,
|
|
3814
|
+
...daemonStartHealthFields(ready, snapshot),
|
|
3605
3815
|
version: snapshot?.version,
|
|
3606
3816
|
expectedVersion
|
|
3607
3817
|
},
|
|
@@ -3905,11 +4115,12 @@ async function daemonEnsure() {
|
|
|
3905
4115
|
return { disabled };
|
|
3906
4116
|
}
|
|
3907
4117
|
const result = await ensureMacDaemon();
|
|
4118
|
+
const snapshot = result.state === "running" ? await requestStatusSnapshot() : null;
|
|
3908
4119
|
if (result.state === "disabled") {
|
|
3909
4120
|
process.stderr.write("[prim] daemon remains explicitly disabled\n");
|
|
3910
4121
|
} else if (result.state === "running") {
|
|
3911
4122
|
process.stderr.write(
|
|
3912
|
-
`${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon ensured under launchd (${result.action})
|
|
4123
|
+
`${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon ensured under launchd (${result.action})`, snapshot)}
|
|
3913
4124
|
`
|
|
3914
4125
|
);
|
|
3915
4126
|
} else {
|
|
@@ -3923,7 +4134,8 @@ async function daemonEnsure() {
|
|
|
3923
4134
|
ensured: result.state === "running",
|
|
3924
4135
|
disabled: result.state === "disabled",
|
|
3925
4136
|
supervised: true,
|
|
3926
|
-
action: result.action
|
|
4137
|
+
action: result.action,
|
|
4138
|
+
...daemonBacklogFields(snapshot)
|
|
3927
4139
|
},
|
|
3928
4140
|
null,
|
|
3929
4141
|
2
|
|
@@ -4958,11 +5170,11 @@ function asRecord(value, path) {
|
|
|
4958
5170
|
}
|
|
4959
5171
|
return value;
|
|
4960
5172
|
}
|
|
4961
|
-
function assertKeys(
|
|
5173
|
+
function assertKeys(record3, path, required, optional = []) {
|
|
4962
5174
|
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
4963
|
-
const missing = required.find((key) => !Object.hasOwn(
|
|
5175
|
+
const missing = required.find((key) => !Object.hasOwn(record3, key));
|
|
4964
5176
|
if (missing !== void 0) contractError(path, `is missing ${missing}`);
|
|
4965
|
-
const extra = Object.keys(
|
|
5177
|
+
const extra = Object.keys(record3).find((key) => !allowed.has(key));
|
|
4966
5178
|
if (extra !== void 0) contractError(path, `contains unexpected field ${extra}`);
|
|
4967
5179
|
}
|
|
4968
5180
|
function asString(value, path, options) {
|
|
@@ -4992,97 +5204,97 @@ function asSha(value, path) {
|
|
|
4992
5204
|
if (!SHA_RE.test(sha)) contractError(path, "must be a canonical lowercase Git commit SHA");
|
|
4993
5205
|
return sha;
|
|
4994
5206
|
}
|
|
4995
|
-
function optionalString(
|
|
4996
|
-
if (!Object.hasOwn(
|
|
4997
|
-
return asString(
|
|
5207
|
+
function optionalString(record3, key, path, max) {
|
|
5208
|
+
if (!Object.hasOwn(record3, key)) return void 0;
|
|
5209
|
+
return asString(record3[key], `${path}.${key}`, { max, nonempty: true });
|
|
4998
5210
|
}
|
|
4999
5211
|
function parseProposal(value, path) {
|
|
5000
|
-
const
|
|
5212
|
+
const record3 = asRecord(value, path);
|
|
5001
5213
|
assertKeys(
|
|
5002
|
-
|
|
5214
|
+
record3,
|
|
5003
5215
|
path,
|
|
5004
5216
|
["_id", "repoFullName", "deadCommitSha", "status", "signals", "lastEvaluatedAt"],
|
|
5005
5217
|
["proposedSha", "proposedTier", "nextEvaluateAt"]
|
|
5006
5218
|
);
|
|
5007
|
-
const repoFullName = asString(
|
|
5219
|
+
const repoFullName = asString(record3.repoFullName, `${path}.repoFullName`, {
|
|
5008
5220
|
max: 200,
|
|
5009
5221
|
nonempty: true
|
|
5010
5222
|
});
|
|
5011
5223
|
if (!REPO_FULL_NAME_RE.test(repoFullName)) {
|
|
5012
5224
|
contractError(`${path}.repoFullName`, "must be canonical owner/repository syntax");
|
|
5013
5225
|
}
|
|
5014
|
-
const status =
|
|
5226
|
+
const status = record3.status;
|
|
5015
5227
|
if (status !== "proposed" && status !== "confirmed" && status !== "verification_failed") {
|
|
5016
5228
|
contractError(`${path}.status`, "is not a review-visible proposal status");
|
|
5017
5229
|
}
|
|
5018
|
-
if (!Array.isArray(
|
|
5230
|
+
if (!Array.isArray(record3.signals) || record3.signals.length > MAX_SIGNALS) {
|
|
5019
5231
|
contractError(`${path}.signals`, "must be a bounded array");
|
|
5020
5232
|
}
|
|
5021
|
-
const signals =
|
|
5233
|
+
const signals = record3.signals.map(
|
|
5022
5234
|
(signal, index) => asString(signal, `${path}.signals[${String(index)}]`, {
|
|
5023
5235
|
max: MAX_SIGNAL_LENGTH,
|
|
5024
5236
|
nonempty: true
|
|
5025
5237
|
})
|
|
5026
5238
|
);
|
|
5027
|
-
const proposedSha = Object.hasOwn(
|
|
5239
|
+
const proposedSha = Object.hasOwn(record3, "proposedSha") ? asSha(record3.proposedSha, `${path}.proposedSha`) : void 0;
|
|
5028
5240
|
let proposedTier;
|
|
5029
|
-
if (Object.hasOwn(
|
|
5030
|
-
if (
|
|
5241
|
+
if (Object.hasOwn(record3, "proposedTier")) {
|
|
5242
|
+
if (record3.proposedTier !== "exact_patch_body" && record3.proposedTier !== "exact_file_set") {
|
|
5031
5243
|
contractError(`${path}.proposedTier`, "is not a supported match tier");
|
|
5032
5244
|
}
|
|
5033
|
-
proposedTier =
|
|
5245
|
+
proposedTier = record3.proposedTier;
|
|
5034
5246
|
}
|
|
5035
|
-
const nextEvaluateAt = Object.hasOwn(
|
|
5247
|
+
const nextEvaluateAt = Object.hasOwn(record3, "nextEvaluateAt") ? asTimestamp(record3.nextEvaluateAt, `${path}.nextEvaluateAt`) : void 0;
|
|
5036
5248
|
return {
|
|
5037
|
-
_id: asString(
|
|
5249
|
+
_id: asString(record3._id, `${path}._id`, { max: 256, nonempty: true }),
|
|
5038
5250
|
repoFullName,
|
|
5039
|
-
deadCommitSha: asSha(
|
|
5251
|
+
deadCommitSha: asSha(record3.deadCommitSha, `${path}.deadCommitSha`),
|
|
5040
5252
|
status,
|
|
5041
5253
|
proposedSha,
|
|
5042
5254
|
proposedTier,
|
|
5043
5255
|
signals,
|
|
5044
|
-
lastEvaluatedAt: asTimestamp(
|
|
5256
|
+
lastEvaluatedAt: asTimestamp(record3.lastEvaluatedAt, `${path}.lastEvaluatedAt`),
|
|
5045
5257
|
nextEvaluateAt
|
|
5046
5258
|
};
|
|
5047
5259
|
}
|
|
5048
5260
|
function parseDecision(value, path) {
|
|
5049
|
-
const
|
|
5050
|
-
assertKeys(
|
|
5051
|
-
const status =
|
|
5261
|
+
const record3 = asRecord(value, path);
|
|
5262
|
+
assertKeys(record3, path, ["_id", "intent", "status", "intentTruncated"], ["shortId"]);
|
|
5263
|
+
const status = record3.status;
|
|
5052
5264
|
if (status !== "provisional" && status !== "abandoned") {
|
|
5053
5265
|
contractError(`${path}.status`, "is not a repairable decision status");
|
|
5054
5266
|
}
|
|
5055
5267
|
return {
|
|
5056
|
-
_id: asString(
|
|
5057
|
-
shortId: optionalString(
|
|
5058
|
-
intent: asString(
|
|
5268
|
+
_id: asString(record3._id, `${path}._id`, { max: 256, nonempty: true }),
|
|
5269
|
+
shortId: optionalString(record3, "shortId", path, 128),
|
|
5270
|
+
intent: asString(record3.intent, `${path}.intent`, { max: MAX_INTENT_LENGTH }),
|
|
5059
5271
|
status,
|
|
5060
|
-
intentTruncated: asBoolean(
|
|
5272
|
+
intentTruncated: asBoolean(record3.intentTruncated, `${path}.intentTruncated`)
|
|
5061
5273
|
};
|
|
5062
5274
|
}
|
|
5063
5275
|
function parseListItem(value, path) {
|
|
5064
|
-
const
|
|
5065
|
-
assertKeys(
|
|
5276
|
+
const record3 = asRecord(value, path);
|
|
5277
|
+
assertKeys(record3, path, [
|
|
5066
5278
|
"proposal",
|
|
5067
5279
|
"decisions",
|
|
5068
5280
|
"affectedDecisionCount",
|
|
5069
5281
|
"decisionsTruncated",
|
|
5070
5282
|
"reviewToken"
|
|
5071
5283
|
]);
|
|
5072
|
-
if (!Array.isArray(
|
|
5284
|
+
if (!Array.isArray(record3.decisions) || record3.decisions.length > MAX_REPAIR_DECISIONS_FOR_CONFIRM) {
|
|
5073
5285
|
contractError(`${path}.decisions`, "must be a bounded array");
|
|
5074
5286
|
}
|
|
5075
|
-
const decisions =
|
|
5287
|
+
const decisions = record3.decisions.map(
|
|
5076
5288
|
(decision, index) => parseDecision(decision, `${path}.decisions[${String(index)}]`)
|
|
5077
5289
|
);
|
|
5078
|
-
const proposal = parseProposal(
|
|
5079
|
-
const decisionsTruncated = asBoolean(
|
|
5080
|
-
const affectedDecisionCount =
|
|
5290
|
+
const proposal = parseProposal(record3.proposal, `${path}.proposal`);
|
|
5291
|
+
const decisionsTruncated = asBoolean(record3.decisionsTruncated, `${path}.decisionsTruncated`);
|
|
5292
|
+
const affectedDecisionCount = record3.affectedDecisionCount === null ? null : asCount(record3.affectedDecisionCount, `${path}.affectedDecisionCount`);
|
|
5081
5293
|
let reviewToken;
|
|
5082
|
-
if (
|
|
5294
|
+
if (record3.reviewToken === null) {
|
|
5083
5295
|
reviewToken = null;
|
|
5084
5296
|
} else {
|
|
5085
|
-
reviewToken = asString(
|
|
5297
|
+
reviewToken = asString(record3.reviewToken, `${path}.reviewToken`, {
|
|
5086
5298
|
max: 64,
|
|
5087
5299
|
nonempty: true
|
|
5088
5300
|
});
|
|
@@ -5115,19 +5327,19 @@ function parseListItem(value, path) {
|
|
|
5115
5327
|
}
|
|
5116
5328
|
function parseListPage(value, pageNumber) {
|
|
5117
5329
|
const path = `page ${String(pageNumber)}`;
|
|
5118
|
-
const
|
|
5119
|
-
assertKeys(
|
|
5120
|
-
if (!Array.isArray(
|
|
5330
|
+
const record3 = asRecord(value, path);
|
|
5331
|
+
assertKeys(record3, path, ["repairs", "isDone", "nextCursor", "truncated"]);
|
|
5332
|
+
if (!Array.isArray(record3.repairs) || record3.repairs.length > MAX_REPAIRS_PER_PAGE) {
|
|
5121
5333
|
contractError(`${path}.repairs`, "must be a bounded array");
|
|
5122
5334
|
}
|
|
5123
|
-
const isDone = asBoolean(
|
|
5124
|
-
const truncated = asBoolean(
|
|
5335
|
+
const isDone = asBoolean(record3.isDone, `${path}.isDone`);
|
|
5336
|
+
const truncated = asBoolean(record3.truncated, `${path}.truncated`);
|
|
5125
5337
|
if (truncated === isDone) contractError(path, "has inconsistent isDone/truncated flags");
|
|
5126
5338
|
let nextCursor;
|
|
5127
|
-
if (
|
|
5339
|
+
if (record3.nextCursor === null) {
|
|
5128
5340
|
nextCursor = null;
|
|
5129
5341
|
} else {
|
|
5130
|
-
nextCursor = asString(
|
|
5342
|
+
nextCursor = asString(record3.nextCursor, `${path}.nextCursor`, {
|
|
5131
5343
|
max: MAX_CURSOR_LENGTH,
|
|
5132
5344
|
nonempty: true
|
|
5133
5345
|
});
|
|
@@ -5135,7 +5347,7 @@ function parseListPage(value, pageNumber) {
|
|
|
5135
5347
|
if (isDone && nextCursor !== null) contractError(path, "is done but includes a next cursor");
|
|
5136
5348
|
if (!isDone && nextCursor === null) contractError(path, "is incomplete without a next cursor");
|
|
5137
5349
|
return {
|
|
5138
|
-
repairs:
|
|
5350
|
+
repairs: record3.repairs.map(
|
|
5139
5351
|
(item, index) => parseListItem(item, `${path}.repairs[${String(index)}]`)
|
|
5140
5352
|
),
|
|
5141
5353
|
isDone,
|
|
@@ -5213,23 +5425,23 @@ function normalizedResolutionInput(proposalId, expectedProposedSha) {
|
|
|
5213
5425
|
return { proposalId, expectedProposedSha: expectedProposedSha.toLowerCase() };
|
|
5214
5426
|
}
|
|
5215
5427
|
function parseResolutionOutcome(value, action) {
|
|
5216
|
-
const
|
|
5428
|
+
const record3 = asRecord(value, "resolution response");
|
|
5217
5429
|
const allowed = action === "confirm" ? ["confirmed", "already_applied", "disabled", "no_op", "stale"] : ["rejected", "already_applied", "no_op", "stale"];
|
|
5218
|
-
if (
|
|
5219
|
-
assertKeys(
|
|
5430
|
+
if (record3.status === "backoff") {
|
|
5431
|
+
assertKeys(record3, "resolution response", ["status", "retryAt"]);
|
|
5220
5432
|
if (action !== "confirm") {
|
|
5221
5433
|
contractError("resolution response.status", "backoff is impossible for reject");
|
|
5222
5434
|
}
|
|
5223
5435
|
return {
|
|
5224
5436
|
status: "backoff",
|
|
5225
|
-
retryAt: asTimestamp(
|
|
5437
|
+
retryAt: asTimestamp(record3.retryAt, "resolution response.retryAt")
|
|
5226
5438
|
};
|
|
5227
5439
|
}
|
|
5228
|
-
assertKeys(
|
|
5229
|
-
if (typeof
|
|
5440
|
+
assertKeys(record3, "resolution response", ["status"]);
|
|
5441
|
+
if (typeof record3.status !== "string" || !allowed.includes(record3.status)) {
|
|
5230
5442
|
contractError("resolution response.status", "is not a supported outcome");
|
|
5231
5443
|
}
|
|
5232
|
-
return { status:
|
|
5444
|
+
return { status: record3.status };
|
|
5233
5445
|
}
|
|
5234
5446
|
function normalizedReviewToken(value) {
|
|
5235
5447
|
if (value === void 0 || !REVIEW_TOKEN_INPUT_RE.test(value)) {
|
|
@@ -5240,18 +5452,18 @@ function normalizedReviewToken(value) {
|
|
|
5240
5452
|
return value.toLowerCase();
|
|
5241
5453
|
}
|
|
5242
5454
|
function parseConflictOutcome(value, action) {
|
|
5243
|
-
const
|
|
5244
|
-
assertKeys(
|
|
5455
|
+
const record3 = asRecord(value, "resolution conflict response");
|
|
5456
|
+
assertKeys(record3, "resolution conflict response", ["status"]);
|
|
5245
5457
|
if (action !== "confirm") {
|
|
5246
5458
|
contractError(
|
|
5247
5459
|
"resolution conflict response.status",
|
|
5248
5460
|
"a review conflict is impossible for reject"
|
|
5249
5461
|
);
|
|
5250
5462
|
}
|
|
5251
|
-
if (
|
|
5463
|
+
if (record3.status !== "review_too_large" && record3.status !== "stale_review") {
|
|
5252
5464
|
contractError("resolution conflict response.status", "is not a supported 409 outcome");
|
|
5253
5465
|
}
|
|
5254
|
-
return { status:
|
|
5466
|
+
return { status: record3.status };
|
|
5255
5467
|
}
|
|
5256
5468
|
async function resolveRepair(proposalId, expectedProposedSha, action, expectedReviewToken, deps = defaultDeps6) {
|
|
5257
5469
|
const normalized = normalizedResolutionInput(proposalId, expectedProposedSha);
|
|
@@ -6306,6 +6518,7 @@ function registerDecisionsCommands(program2) {
|
|
|
6306
6518
|
|
|
6307
6519
|
// src/commands/doctor.ts
|
|
6308
6520
|
import { existsSync as existsSync6 } from "fs";
|
|
6521
|
+
import { Option as Option4 } from "commander";
|
|
6309
6522
|
|
|
6310
6523
|
// src/protocol/decision-lifecycle.ts
|
|
6311
6524
|
var DECISION_LIFECYCLE_PROTOCOL_VERSION = 2;
|
|
@@ -6330,28 +6543,35 @@ function parseOrganizationBinding(value) {
|
|
|
6330
6543
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
6331
6544
|
return { state: "server_contract_unavailable" };
|
|
6332
6545
|
}
|
|
6333
|
-
const
|
|
6334
|
-
if (
|
|
6546
|
+
const record3 = value;
|
|
6547
|
+
if (record3.authenticated !== true || record3.organizationBindingVersion !== ORGANIZATION_BINDING_VERSION || !isCaptureAuthorityKind(record3.captureAuthorityKind)) {
|
|
6335
6548
|
return { state: "server_contract_unavailable" };
|
|
6336
6549
|
}
|
|
6337
|
-
if (
|
|
6550
|
+
if (record3.organizationId === null && record3.workosOrganizationId === null) {
|
|
6338
6551
|
return { state: "no_current_organization" };
|
|
6339
6552
|
}
|
|
6340
|
-
if (safeOrganizationId(
|
|
6553
|
+
if (safeOrganizationId(record3.organizationId) && record3.workosOrganizationId === null) {
|
|
6341
6554
|
return { state: "organization_identity_unreconciled" };
|
|
6342
6555
|
}
|
|
6343
|
-
if (!safeOrganizationId(
|
|
6556
|
+
if (!safeOrganizationId(record3.organizationId) || !safeOrganizationId(record3.workosOrganizationId)) {
|
|
6344
6557
|
return { state: "server_contract_unavailable" };
|
|
6345
6558
|
}
|
|
6346
6559
|
return {
|
|
6347
6560
|
state: "current",
|
|
6348
6561
|
binding: {
|
|
6349
|
-
captureAuthorityKind:
|
|
6350
|
-
organizationId:
|
|
6351
|
-
workosOrganizationId:
|
|
6562
|
+
captureAuthorityKind: record3.captureAuthorityKind,
|
|
6563
|
+
organizationId: record3.organizationId,
|
|
6564
|
+
workosOrganizationId: record3.workosOrganizationId
|
|
6352
6565
|
}
|
|
6353
6566
|
};
|
|
6354
6567
|
}
|
|
6568
|
+
function summarizeRetainedBuckets(retained) {
|
|
6569
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6570
|
+
for (const item of retained) {
|
|
6571
|
+
counts.set(item.reason, (counts.get(item.reason) ?? 0) + 1);
|
|
6572
|
+
}
|
|
6573
|
+
return [...counts].sort(([left], [right]) => left.localeCompare(right)).map(([reason, count]) => `${reason}:${String(count)}`).join(", ");
|
|
6574
|
+
}
|
|
6355
6575
|
function classifyJournalBuckets(buckets, binding) {
|
|
6356
6576
|
const deliverableBuckets = /* @__PURE__ */ new Set();
|
|
6357
6577
|
const retainedBuckets = [];
|
|
@@ -6423,7 +6643,7 @@ async function inspectJournalDelivery(buckets, options = {}) {
|
|
|
6423
6643
|
}
|
|
6424
6644
|
|
|
6425
6645
|
// src/commands/hermes-install.ts
|
|
6426
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, rmSync as rmSync2, statSync } from "fs";
|
|
6646
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync2 } from "fs";
|
|
6427
6647
|
import { homedir as homedir4 } from "os";
|
|
6428
6648
|
import { join as join6 } from "path";
|
|
6429
6649
|
import { Document, isMap, isSeq, parseAllDocuments, stringify, visit } from "yaml";
|
|
@@ -6831,7 +7051,7 @@ function readConfigSnapshot(path) {
|
|
|
6831
7051
|
if (!existsSync5(path)) return { exists: false, mode: CONFIG_MODE, raw: "" };
|
|
6832
7052
|
return {
|
|
6833
7053
|
exists: true,
|
|
6834
|
-
mode:
|
|
7054
|
+
mode: statSync2(path).mode & 511,
|
|
6835
7055
|
raw: readFileSync5(path, "utf8")
|
|
6836
7056
|
};
|
|
6837
7057
|
}
|
|
@@ -6973,7 +7193,6 @@ function registerHermesCommands(program2) {
|
|
|
6973
7193
|
// src/commands/doctor.ts
|
|
6974
7194
|
var DAEMON_PROBE_TIMEOUT_MS = 500;
|
|
6975
7195
|
var CONNECTIVITY_TIMEOUT_MS = 3e3;
|
|
6976
|
-
var MS_PER_SECOND = 1e3;
|
|
6977
7196
|
var STALE_PENDING_MS = 3e4;
|
|
6978
7197
|
var EXIT_UNHEALTHY = 1;
|
|
6979
7198
|
function classifyDoctor(checks) {
|
|
@@ -7095,8 +7314,19 @@ function classifyDaemonHealth(snapshot, options = {}) {
|
|
|
7095
7314
|
};
|
|
7096
7315
|
}
|
|
7097
7316
|
if (!snapshot.ingestion?.healthy) {
|
|
7098
|
-
|
|
7099
|
-
|
|
7317
|
+
if (options.backlogExpected && deliveryBacklogState(snapshot.ingestion) === "draining") {
|
|
7318
|
+
const ingestionStatus2 = options.ingestionStatus ?? decisionIngestionStatus(process.cwd());
|
|
7319
|
+
const live = options.backlog;
|
|
7320
|
+
const progress = live && live.pendingCount === 0 && live.pendingSampled !== true ? "backlog drained; daemon health refreshes on its next sweep" : deliveryBacklogSummary(snapshot.ingestion, options.now, live);
|
|
7321
|
+
return {
|
|
7322
|
+
name: "daemon",
|
|
7323
|
+
status: "warn",
|
|
7324
|
+
detail: `supervised and live${snapshot.version ? ` \xB7 v${snapshot.version}` : ""} \xB7 Decision ingestion ${ingestionStatus2} \xB7 ${progress}`
|
|
7325
|
+
};
|
|
7326
|
+
}
|
|
7327
|
+
const counts = options.backlog ?? snapshot.ingestion;
|
|
7328
|
+
const pending = counts?.pendingCount ?? 0;
|
|
7329
|
+
const pendingLabel = counts?.pendingSampled ? `at least ${String(pending)}` : String(pending);
|
|
7100
7330
|
return {
|
|
7101
7331
|
name: "daemon",
|
|
7102
7332
|
status: "fail",
|
|
@@ -7113,17 +7343,23 @@ function classifyDaemonHealth(snapshot, options = {}) {
|
|
|
7113
7343
|
detail: `supervised and healthy${snapshot.version ? ` \xB7 v${snapshot.version}` : ""} \xB7 Decision ingestion ${ingestionStatus}`
|
|
7114
7344
|
};
|
|
7115
7345
|
}
|
|
7116
|
-
async function
|
|
7346
|
+
async function probeDelivery() {
|
|
7347
|
+
const stats = pendingJournalStats();
|
|
7348
|
+
const now = Date.now();
|
|
7349
|
+
const disabled = daemonExplicitlyDisabled();
|
|
7350
|
+
const expectedVersion = packageVersion();
|
|
7117
7351
|
let service;
|
|
7118
7352
|
if (process.platform === "darwin") {
|
|
7119
7353
|
try {
|
|
7120
7354
|
service = getLaunchdService();
|
|
7121
7355
|
} catch (error) {
|
|
7122
|
-
const detail = boundedHealthError(error instanceof Error ? error.message : String(error));
|
|
7123
7356
|
return {
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7357
|
+
snapshot: null,
|
|
7358
|
+
launchdError: error instanceof Error ? error.message : String(error),
|
|
7359
|
+
disabled,
|
|
7360
|
+
expectedVersion,
|
|
7361
|
+
stats,
|
|
7362
|
+
now
|
|
7127
7363
|
};
|
|
7128
7364
|
}
|
|
7129
7365
|
}
|
|
@@ -7132,53 +7368,74 @@ async function checkDaemon() {
|
|
|
7132
7368
|
{ callerEnv: getSiteUrl() },
|
|
7133
7369
|
{ timeoutMs: DAEMON_PROBE_TIMEOUT_MS }
|
|
7134
7370
|
);
|
|
7135
|
-
return
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7371
|
+
return { snapshot, service, disabled, expectedVersion, stats, now };
|
|
7372
|
+
}
|
|
7373
|
+
function daemonCheckVouchesForDrain(check) {
|
|
7374
|
+
return check.name === "daemon" && check.status !== "fail";
|
|
7375
|
+
}
|
|
7376
|
+
function classifyDelivery(probe, options = {}) {
|
|
7377
|
+
const { stats, now } = probe;
|
|
7378
|
+
const launchdDetail = probe.launchdError === void 0 ? void 0 : boundedHealthError(probe.launchdError);
|
|
7379
|
+
const daemon = probe.launchdError === void 0 ? classifyDaemonHealth(probe.snapshot, {
|
|
7380
|
+
disabled: probe.disabled,
|
|
7381
|
+
service: probe.service,
|
|
7382
|
+
ingestionStatus: probe.ingestionStatus,
|
|
7383
|
+
expectedVersion: probe.expectedVersion,
|
|
7384
|
+
backlogExpected: options.backlogExpected,
|
|
7385
|
+
backlog: {
|
|
7386
|
+
pendingCount: stats.pendingCount,
|
|
7387
|
+
pendingSampled: stats.sampled,
|
|
7388
|
+
oldestPendingAt: stats.oldestPendingAt
|
|
7389
|
+
},
|
|
7390
|
+
now
|
|
7391
|
+
}) : {
|
|
7392
|
+
name: "daemon",
|
|
7393
|
+
status: "fail",
|
|
7394
|
+
detail: `launchd status unavailable${launchdDetail ? `: ${launchdDetail}` : ""}`
|
|
7395
|
+
};
|
|
7396
|
+
const journal = classifyJournal(stats, now, {
|
|
7397
|
+
backlogExpected: options.backlogExpected === true && daemonCheckVouchesForDrain(daemon)
|
|
7139
7398
|
});
|
|
7399
|
+
return [daemon, journal, classifyStranded(stats)];
|
|
7140
7400
|
}
|
|
7141
|
-
function
|
|
7142
|
-
const stats = pendingJournalStats();
|
|
7401
|
+
function classifyJournal(stats, now, options = {}) {
|
|
7143
7402
|
const pending = stats.pendingCount;
|
|
7144
7403
|
const pendingLabel = stats.sampled ? `at least ${String(pending)}` : String(pending);
|
|
7404
|
+
const backlog = (detail) => options.backlogExpected ? {
|
|
7405
|
+
name: "journal",
|
|
7406
|
+
status: "warn",
|
|
7407
|
+
detail: `${formatPendingBacklog(
|
|
7408
|
+
{
|
|
7409
|
+
pendingCount: pending,
|
|
7410
|
+
pendingSampled: stats.sampled,
|
|
7411
|
+
oldestPendingAt: stats.oldestPendingAt
|
|
7412
|
+
},
|
|
7413
|
+
now
|
|
7414
|
+
)} \u2014 draining in the background`
|
|
7415
|
+
} : { name: "journal", status: "fail", detail };
|
|
7145
7416
|
if (pending === 0) {
|
|
7146
7417
|
if (stats.sampled) {
|
|
7147
|
-
return
|
|
7148
|
-
name: "journal",
|
|
7149
|
-
status: "fail",
|
|
7150
|
-
detail: "bounded journal sample could not prove the queue is empty"
|
|
7151
|
-
};
|
|
7418
|
+
return backlog("bounded journal sample could not prove the queue is empty");
|
|
7152
7419
|
}
|
|
7153
7420
|
return { name: "journal", status: "ok", detail: "no pending moves" };
|
|
7154
7421
|
}
|
|
7155
7422
|
if (stats.oldestPendingAt === void 0) {
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
status: "fail",
|
|
7159
|
-
detail: `${pendingLabel} pending with no readable capture timestamp`
|
|
7160
|
-
};
|
|
7423
|
+
const detail = `${pendingLabel} pending with no readable capture timestamp`;
|
|
7424
|
+
return stats.sampled ? backlog(detail) : { name: "journal", status: "fail", detail };
|
|
7161
7425
|
}
|
|
7162
|
-
const oldestMs =
|
|
7426
|
+
const oldestMs = now - stats.oldestPendingAt;
|
|
7163
7427
|
const oldestS = Math.round(oldestMs / MS_PER_SECOND);
|
|
7164
7428
|
if (oldestMs > STALE_PENDING_MS) {
|
|
7165
|
-
return
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
detail: `${pendingLabel} pending, oldest observed ${String(oldestS)}s \u2014 30s delivery SLA missed`
|
|
7169
|
-
};
|
|
7429
|
+
return backlog(
|
|
7430
|
+
`${pendingLabel} pending, oldest observed ${String(oldestS)}s \u2014 30s delivery SLA missed`
|
|
7431
|
+
);
|
|
7170
7432
|
}
|
|
7171
7433
|
if (stats.sampled) {
|
|
7172
|
-
return {
|
|
7173
|
-
name: "journal",
|
|
7174
|
-
status: "fail",
|
|
7175
|
-
detail: `${pendingLabel} pending in bounded sample; 30s delivery SLA cannot be proven`
|
|
7176
|
-
};
|
|
7434
|
+
return backlog(`${pendingLabel} pending in bounded sample; 30s delivery SLA cannot be proven`);
|
|
7177
7435
|
}
|
|
7178
7436
|
return { name: "journal", status: "ok", detail: `${String(pending)} pending, draining` };
|
|
7179
7437
|
}
|
|
7180
|
-
function
|
|
7181
|
-
const stats = pendingJournalStats();
|
|
7438
|
+
function classifyStranded(stats) {
|
|
7182
7439
|
if (stats.strandedFileCount === 0 && !stats.strandedSampled) {
|
|
7183
7440
|
return { name: "stranded", status: "ok", detail: "none" };
|
|
7184
7441
|
}
|
|
@@ -7204,15 +7461,10 @@ function classifyJournalOrganization(bucketCount, retainedBuckets) {
|
|
|
7204
7461
|
detail: "all pending buckets match the active credential"
|
|
7205
7462
|
};
|
|
7206
7463
|
}
|
|
7207
|
-
const reasonCounts = /* @__PURE__ */ new Map();
|
|
7208
|
-
for (const item of retainedBuckets) {
|
|
7209
|
-
reasonCounts.set(item.reason, (reasonCounts.get(item.reason) ?? 0) + 1);
|
|
7210
|
-
}
|
|
7211
|
-
const reasons = [...reasonCounts].sort(([left], [right]) => left.localeCompare(right)).map(([reason, count]) => `${reason}:${String(count)}`).join(", ");
|
|
7212
7464
|
return {
|
|
7213
7465
|
name: "journal-org",
|
|
7214
7466
|
status: "fail",
|
|
7215
|
-
detail: `${String(retainedBuckets.length)} bucket(s) retained (${
|
|
7467
|
+
detail: `${String(retainedBuckets.length)} bucket(s) retained (${summarizeRetainedBuckets(retainedBuckets)})`
|
|
7216
7468
|
};
|
|
7217
7469
|
}
|
|
7218
7470
|
async function checkJournalOrganization() {
|
|
@@ -7623,30 +7875,37 @@ function refreshOwnedGlobalHooksForHealth() {
|
|
|
7623
7875
|
} catch {
|
|
7624
7876
|
}
|
|
7625
7877
|
}
|
|
7626
|
-
async function
|
|
7878
|
+
async function independentChecks() {
|
|
7627
7879
|
refreshOwnedGlobalHooksForHealth();
|
|
7628
7880
|
const backend = await checkBackend();
|
|
7629
|
-
return
|
|
7630
|
-
checkAuth(),
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7881
|
+
return {
|
|
7882
|
+
before: [checkAuth()],
|
|
7883
|
+
after: [
|
|
7884
|
+
await checkJournalOrganization(),
|
|
7885
|
+
checkFeedbackHooks(),
|
|
7886
|
+
...checkAgentHooks(),
|
|
7887
|
+
checkHookRuntime(),
|
|
7888
|
+
await checkRepositoryBinding(),
|
|
7889
|
+
checkManagedHook("post-commit", inspectEffectivePostCommitHook),
|
|
7890
|
+
checkManagedHook("post-rewrite", inspectEffectivePostRewriteHook),
|
|
7891
|
+
...backend,
|
|
7892
|
+
await checkFeedbackCapability()
|
|
7893
|
+
]
|
|
7894
|
+
};
|
|
7895
|
+
}
|
|
7896
|
+
var DEFAULT_DOCTOR_PROBES = {
|
|
7897
|
+
delivery: probeDelivery,
|
|
7898
|
+
independent: independentChecks
|
|
7899
|
+
};
|
|
7900
|
+
async function collectChecks(options, probes) {
|
|
7901
|
+
const { before, after } = await probes.independent();
|
|
7902
|
+
return [...before, ...classifyDelivery(await probes.delivery(), options), ...after];
|
|
7644
7903
|
}
|
|
7645
7904
|
function icon(status) {
|
|
7646
7905
|
return status === "ok" ? "\u2713" : status === "warn" ? "\u26A0" : "\u2717";
|
|
7647
7906
|
}
|
|
7648
|
-
async function runDoctor() {
|
|
7649
|
-
const checks = await collectChecks();
|
|
7907
|
+
async function runDoctor(options, probes) {
|
|
7908
|
+
const checks = await collectChecks(options, probes);
|
|
7650
7909
|
const { json, exitCode } = classifyDoctor(checks);
|
|
7651
7910
|
const headline = json.status === "ok" ? "\u2713 healthy" : json.status === "warn" ? "\u26A0 degraded" : "\u2717 unhealthy";
|
|
7652
7911
|
process.stderr.write(`[prim] doctor: ${headline}
|
|
@@ -7660,11 +7919,14 @@ async function runDoctor() {
|
|
|
7660
7919
|
process.exitCode = exitCode;
|
|
7661
7920
|
}
|
|
7662
7921
|
}
|
|
7663
|
-
function registerDoctorCommands(program2) {
|
|
7922
|
+
function registerDoctorCommands(program2, dependencies = {}) {
|
|
7664
7923
|
program2.command("doctor").description(
|
|
7665
7924
|
"Check capture and feedback health end to end (auth, supervisor, delivery, server)"
|
|
7666
|
-
).action(async () => {
|
|
7667
|
-
await runDoctor(
|
|
7925
|
+
).addOption(new Option4("--expect-backlog").hideHelp()).action(async (opts) => {
|
|
7926
|
+
await runDoctor(
|
|
7927
|
+
{ backlogExpected: opts.expectBacklog === true },
|
|
7928
|
+
dependencies.probes ?? DEFAULT_DOCTOR_PROBES
|
|
7929
|
+
);
|
|
7668
7930
|
});
|
|
7669
7931
|
}
|
|
7670
7932
|
|
|
@@ -7673,20 +7935,20 @@ import { existsSync as existsSync8, mkdirSync as mkdirSync5, unlinkSync as unlin
|
|
|
7673
7935
|
import { join as join9 } from "path";
|
|
7674
7936
|
|
|
7675
7937
|
// src/flusher.ts
|
|
7676
|
-
import { createReadStream, renameSync as renameSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
7938
|
+
import { createReadStream, renameSync as renameSync3, statSync as statSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
7677
7939
|
import { dirname as dirname4, join as join8 } from "path";
|
|
7678
7940
|
|
|
7679
7941
|
// src/dead-letter.ts
|
|
7680
7942
|
import { createHash as createHash3, randomBytes as randomBytes4 } from "crypto";
|
|
7681
7943
|
import {
|
|
7682
|
-
constants as
|
|
7944
|
+
constants as constants3,
|
|
7683
7945
|
chmodSync as chmodSync2,
|
|
7684
|
-
closeSync as
|
|
7946
|
+
closeSync as closeSync3,
|
|
7685
7947
|
existsSync as existsSync7,
|
|
7686
7948
|
fchmodSync,
|
|
7687
7949
|
fsyncSync,
|
|
7688
7950
|
mkdirSync as mkdirSync4,
|
|
7689
|
-
openSync as
|
|
7951
|
+
openSync as openSync3,
|
|
7690
7952
|
renameSync as renameSync2,
|
|
7691
7953
|
rmSync as rmSync3,
|
|
7692
7954
|
writeFileSync as writeFileSync3
|
|
@@ -7709,7 +7971,7 @@ function ensureDeadLetterDirectory(flushingPath, persistDirectory) {
|
|
|
7709
7971
|
persistDirectory(dirname3(directory));
|
|
7710
7972
|
return directory;
|
|
7711
7973
|
}
|
|
7712
|
-
function persistDeadLetter(directory, path, quarantineId,
|
|
7974
|
+
function persistDeadLetter(directory, path, quarantineId, record3, persistDirectory) {
|
|
7713
7975
|
if (existsSync7(path)) {
|
|
7714
7976
|
persistDirectory(directory);
|
|
7715
7977
|
return;
|
|
@@ -7718,16 +7980,16 @@ function persistDeadLetter(directory, path, quarantineId, record2, persistDirect
|
|
|
7718
7980
|
directory,
|
|
7719
7981
|
`.${quarantineId}.${String(process.pid)}.${randomBytes4(8).toString("hex")}.tmp`
|
|
7720
7982
|
);
|
|
7721
|
-
const flags =
|
|
7983
|
+
const flags = constants3.O_CREAT | constants3.O_EXCL | constants3.O_WRONLY | constants3.O_NOFOLLOW;
|
|
7722
7984
|
try {
|
|
7723
|
-
const fd =
|
|
7985
|
+
const fd = openSync3(temporaryPath, flags, FILE_MODE);
|
|
7724
7986
|
try {
|
|
7725
7987
|
fchmodSync(fd, FILE_MODE);
|
|
7726
|
-
writeFileSync3(fd, `${JSON.stringify(
|
|
7988
|
+
writeFileSync3(fd, `${JSON.stringify(record3)}
|
|
7727
7989
|
`, "utf8");
|
|
7728
7990
|
fsyncSync(fd);
|
|
7729
7991
|
} finally {
|
|
7730
|
-
|
|
7992
|
+
closeSync3(fd);
|
|
7731
7993
|
}
|
|
7732
7994
|
renameSync2(temporaryPath, path);
|
|
7733
7995
|
persistDirectory(directory);
|
|
@@ -7739,22 +8001,22 @@ function quarantineMove(flushingPath, move, reason, now = Date.now, options = {}
|
|
|
7739
8001
|
const persistDirectory = options.syncDirectory ?? syncDirectory;
|
|
7740
8002
|
const directory = ensureDeadLetterDirectory(flushingPath, persistDirectory);
|
|
7741
8003
|
const serializedMove = JSON.stringify(move);
|
|
7742
|
-
const
|
|
8004
|
+
const record3 = {
|
|
7743
8005
|
version: 1,
|
|
7744
8006
|
quarantineId: createHash3("sha256").update(serializedMove).digest("hex"),
|
|
7745
8007
|
quarantinedAt: now(),
|
|
7746
8008
|
reason,
|
|
7747
8009
|
move
|
|
7748
8010
|
};
|
|
7749
|
-
const path = join7(directory, `${
|
|
7750
|
-
persistDeadLetter(directory, path,
|
|
7751
|
-
return
|
|
8011
|
+
const path = join7(directory, `${record3.quarantineId}.json`);
|
|
8012
|
+
persistDeadLetter(directory, path, record3.quarantineId, record3, persistDirectory);
|
|
8013
|
+
return record3;
|
|
7752
8014
|
}
|
|
7753
8015
|
function quarantineRawLine(flushingPath, rawLine, now = Date.now, options = {}) {
|
|
7754
8016
|
const persistDirectory = options.syncDirectory ?? syncDirectory;
|
|
7755
8017
|
const directory = ensureDeadLetterDirectory(flushingPath, persistDirectory);
|
|
7756
8018
|
const bytes = Buffer.from(rawLine);
|
|
7757
|
-
const
|
|
8019
|
+
const record3 = {
|
|
7758
8020
|
version: 1,
|
|
7759
8021
|
recordKind: "raw_line_v1",
|
|
7760
8022
|
quarantineId: createHash3("sha256").update(bytes).digest("hex"),
|
|
@@ -7767,18 +8029,22 @@ function quarantineRawLine(flushingPath, rawLine, now = Date.now, options = {})
|
|
|
7767
8029
|
persistDeadLetter(
|
|
7768
8030
|
directory,
|
|
7769
8031
|
deadLetterPathForRawLine(flushingPath, bytes),
|
|
7770
|
-
|
|
7771
|
-
|
|
8032
|
+
record3.quarantineId,
|
|
8033
|
+
record3,
|
|
7772
8034
|
persistDirectory
|
|
7773
8035
|
);
|
|
7774
|
-
return
|
|
8036
|
+
return record3;
|
|
7775
8037
|
}
|
|
7776
8038
|
|
|
7777
8039
|
// src/flusher.ts
|
|
7778
8040
|
var BATCH_SIZE = 500;
|
|
8041
|
+
var BATCH_MAX_BYTES = 1048576;
|
|
7779
8042
|
var HTTP_TIMEOUT_MS = 1e4;
|
|
7780
8043
|
var OPPORTUNISTIC_FLUSH_AFTER_MS = 6e4;
|
|
7781
8044
|
var ORPHAN_QUARANTINE_MS = 6e4;
|
|
8045
|
+
function exceedsBatchBytes(count, bytes, nextBytes, maxBytes) {
|
|
8046
|
+
return count > 0 && bytes + nextBytes > maxBytes;
|
|
8047
|
+
}
|
|
7782
8048
|
function addDrainCounts(left, right) {
|
|
7783
8049
|
return {
|
|
7784
8050
|
flushed: left.flushed + right.flushed,
|
|
@@ -7789,8 +8055,8 @@ function collectionScopeRoots(batch) {
|
|
|
7789
8055
|
return batch.flatMap((move) => {
|
|
7790
8056
|
const env = move.env;
|
|
7791
8057
|
if (typeof env !== "object" || env === null || Array.isArray(env)) return [];
|
|
7792
|
-
const
|
|
7793
|
-
const root = typeof
|
|
8058
|
+
const record3 = env;
|
|
8059
|
+
const root = typeof record3.gitRoot === "string" ? record3.gitRoot : record3.cwd;
|
|
7794
8060
|
return typeof root === "string" && root.length > 0 ? [root] : [];
|
|
7795
8061
|
});
|
|
7796
8062
|
}
|
|
@@ -7798,8 +8064,8 @@ function isVersionedInvalidMoveDisposition(body) {
|
|
|
7798
8064
|
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
7799
8065
|
return false;
|
|
7800
8066
|
}
|
|
7801
|
-
const
|
|
7802
|
-
return Object.keys(
|
|
8067
|
+
const record3 = body;
|
|
8068
|
+
return Object.keys(record3).length === 2 && record3.error === "invalid_move" && record3.errorVersion === 1;
|
|
7803
8069
|
}
|
|
7804
8070
|
function deadLetterReason(error) {
|
|
7805
8071
|
if (error instanceof InvalidJournalEnvelopeError) {
|
|
@@ -7822,23 +8088,31 @@ function deadLetterReason(error) {
|
|
|
7822
8088
|
}
|
|
7823
8089
|
return void 0;
|
|
7824
8090
|
}
|
|
7825
|
-
|
|
8091
|
+
function mayBeBatchSizeFailure(error) {
|
|
8092
|
+
if (error instanceof HttpError) {
|
|
8093
|
+
return error.status >= 500 && error.status !== 503;
|
|
8094
|
+
}
|
|
8095
|
+
return error instanceof Error && error.name === "TimeoutError";
|
|
8096
|
+
}
|
|
8097
|
+
async function* rawJournalLines(input, offset) {
|
|
7826
8098
|
let pending = [];
|
|
7827
8099
|
let pendingBytes = 0;
|
|
8100
|
+
let end = offset;
|
|
7828
8101
|
for await (const value of input) {
|
|
7829
8102
|
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
7830
8103
|
let start = 0;
|
|
7831
8104
|
let newline = chunk.indexOf(10, start);
|
|
7832
8105
|
while (newline !== -1) {
|
|
7833
8106
|
const tail = chunk.subarray(start, newline + 1);
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
} else {
|
|
8107
|
+
let line = tail;
|
|
8108
|
+
if (pending.length > 0) {
|
|
7837
8109
|
pending.push(tail);
|
|
7838
|
-
|
|
8110
|
+
line = Buffer.concat(pending, pendingBytes + tail.length);
|
|
7839
8111
|
pending = [];
|
|
7840
8112
|
pendingBytes = 0;
|
|
7841
8113
|
}
|
|
8114
|
+
end += line.length;
|
|
8115
|
+
yield { bytes: line, end };
|
|
7842
8116
|
start = newline + 1;
|
|
7843
8117
|
newline = chunk.indexOf(10, start);
|
|
7844
8118
|
}
|
|
@@ -7849,21 +8123,46 @@ async function* rawJournalLines(input) {
|
|
|
7849
8123
|
}
|
|
7850
8124
|
}
|
|
7851
8125
|
if (pendingBytes > 0) {
|
|
7852
|
-
|
|
8126
|
+
const bytes = pending.length === 1 ? pending[0] : Buffer.concat(pending, pendingBytes);
|
|
8127
|
+
yield { bytes, end: end + pendingBytes };
|
|
7853
8128
|
}
|
|
7854
8129
|
}
|
|
8130
|
+
var drainProgressWarned = false;
|
|
7855
8131
|
async function drainFlushingPath(flushingPath, client, binding, options = {}) {
|
|
8132
|
+
const rotation = rotationIdentity(statSync3(flushingPath));
|
|
8133
|
+
const resumeAt = drainResumeOffset(flushingPath, rotation);
|
|
8134
|
+
const recordProgress = (end) => {
|
|
8135
|
+
if (end === void 0 || end >= rotation.size) {
|
|
8136
|
+
return;
|
|
8137
|
+
}
|
|
8138
|
+
try {
|
|
8139
|
+
writeDrainCheckpoint(flushingPath, { v: 2, offset: end, ...rotation });
|
|
8140
|
+
drainProgressWarned = false;
|
|
8141
|
+
} catch (error) {
|
|
8142
|
+
if (!drainProgressWarned) {
|
|
8143
|
+
drainProgressWarned = true;
|
|
8144
|
+
process.stderr.write(
|
|
8145
|
+
`[prim] could not record drain progress: ${error instanceof Error ? error.message : String(error)}
|
|
8146
|
+
`
|
|
8147
|
+
);
|
|
8148
|
+
}
|
|
8149
|
+
}
|
|
8150
|
+
};
|
|
8151
|
+
const reportProgress = (delta) => {
|
|
8152
|
+
options.onProgress?.(delta);
|
|
8153
|
+
return delta;
|
|
8154
|
+
};
|
|
7856
8155
|
const postBatch = async (batch2) => {
|
|
8156
|
+
const moves = batch2.map((entry) => entry.move);
|
|
7857
8157
|
try {
|
|
7858
|
-
const request = buildOrganizationBoundMoveRequest(
|
|
8158
|
+
const request = buildOrganizationBoundMoveRequest(moves, binding);
|
|
7859
8159
|
const response = await client.post("/api/cli/moves/ingest", request, {
|
|
7860
8160
|
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
|
|
7861
8161
|
});
|
|
7862
|
-
requireDurableIngestAcknowledgement(response,
|
|
7863
|
-
return { flushed: batch2.length, quarantined: 0 };
|
|
8162
|
+
requireDurableIngestAcknowledgement(response, moves.length, collectionScopeRoots(moves));
|
|
7864
8163
|
} catch (error) {
|
|
7865
8164
|
const reason = deadLetterReason(error);
|
|
7866
|
-
if (!reason) {
|
|
8165
|
+
if (!(reason || mayBeBatchSizeFailure(error))) {
|
|
7867
8166
|
throw error;
|
|
7868
8167
|
}
|
|
7869
8168
|
if (batch2.length > 1) {
|
|
@@ -7872,33 +8171,46 @@ async function drainFlushingPath(flushingPath, client, binding, options = {}) {
|
|
|
7872
8171
|
const right = await postBatch(batch2.slice(midpoint));
|
|
7873
8172
|
return addDrainCounts(left, right);
|
|
7874
8173
|
}
|
|
7875
|
-
|
|
8174
|
+
if (!reason) {
|
|
8175
|
+
throw error;
|
|
8176
|
+
}
|
|
8177
|
+
const [entry] = batch2;
|
|
7876
8178
|
const quarantined = quarantineMove(
|
|
7877
8179
|
flushingPath,
|
|
7878
|
-
move,
|
|
8180
|
+
entry.move,
|
|
7879
8181
|
reason,
|
|
7880
8182
|
Date.now,
|
|
7881
8183
|
options.deadLetterPersistence
|
|
7882
8184
|
);
|
|
8185
|
+
recordProgress(entry.end);
|
|
7883
8186
|
process.stderr.write(
|
|
7884
8187
|
`[prim] quarantined rejected move ${quarantined.quarantineId.slice(0, 12)} (${reason})
|
|
7885
8188
|
`
|
|
7886
8189
|
);
|
|
7887
|
-
return { flushed: 0, quarantined: 1 };
|
|
8190
|
+
return reportProgress({ flushed: 0, quarantined: 1 });
|
|
7888
8191
|
}
|
|
8192
|
+
recordProgress(batch2.at(-1)?.end);
|
|
8193
|
+
return reportProgress({ flushed: batch2.length, quarantined: 0 });
|
|
7889
8194
|
};
|
|
7890
|
-
const input = createReadStream(flushingPath);
|
|
8195
|
+
const input = createReadStream(flushingPath, { start: resumeAt });
|
|
7891
8196
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
7892
8197
|
let batch = [];
|
|
8198
|
+
let batchBytes = 0;
|
|
7893
8199
|
let counts = { flushed: 0, quarantined: 0 };
|
|
8200
|
+
const postPending = async () => {
|
|
8201
|
+
counts = addDrainCounts(counts, await postBatch(batch));
|
|
8202
|
+
batch = [];
|
|
8203
|
+
batchBytes = 0;
|
|
8204
|
+
};
|
|
7894
8205
|
try {
|
|
7895
|
-
for await (const rawLine of rawJournalLines(input)) {
|
|
8206
|
+
for await (const { bytes: rawLine, end } of rawJournalLines(input, resumeAt)) {
|
|
7896
8207
|
if (rawLine.length === 1 && rawLine[0] === 10 || rawLine.length === 2 && rawLine[0] === 13 && rawLine[1] === 10) {
|
|
7897
8208
|
continue;
|
|
7898
8209
|
}
|
|
8210
|
+
let move;
|
|
7899
8211
|
try {
|
|
7900
8212
|
const line = decoder.decode(rawLine);
|
|
7901
|
-
|
|
8213
|
+
move = JSON.parse(line);
|
|
7902
8214
|
} catch {
|
|
7903
8215
|
const quarantined = quarantineRawLine(
|
|
7904
8216
|
flushingPath,
|
|
@@ -7910,24 +8222,35 @@ async function drainFlushingPath(flushingPath, client, binding, options = {}) {
|
|
|
7910
8222
|
`[prim] quarantined invalid journal line ${quarantined.quarantineId.slice(0, 12)} (invalid_move)
|
|
7911
8223
|
`
|
|
7912
8224
|
);
|
|
7913
|
-
counts
|
|
8225
|
+
counts = addDrainCounts(counts, reportProgress({ flushed: 0, quarantined: 1 }));
|
|
8226
|
+
if (batch.length === 0) {
|
|
8227
|
+
recordProgress(end);
|
|
8228
|
+
}
|
|
7914
8229
|
continue;
|
|
7915
8230
|
}
|
|
7916
|
-
if (batch.length
|
|
7917
|
-
|
|
7918
|
-
|
|
8231
|
+
if (exceedsBatchBytes(batch.length, batchBytes, rawLine.length, BATCH_MAX_BYTES)) {
|
|
8232
|
+
await postPending();
|
|
8233
|
+
}
|
|
8234
|
+
batch.push({ move, end });
|
|
8235
|
+
batchBytes += rawLine.length;
|
|
8236
|
+
if (batch.length >= BATCH_SIZE) {
|
|
8237
|
+
await postPending();
|
|
7919
8238
|
}
|
|
7920
8239
|
}
|
|
7921
8240
|
if (batch.length > 0) {
|
|
7922
|
-
|
|
8241
|
+
await postPending();
|
|
7923
8242
|
}
|
|
7924
8243
|
} finally {
|
|
7925
8244
|
input.destroy();
|
|
7926
8245
|
}
|
|
7927
8246
|
unlinkSync3(flushingPath);
|
|
8247
|
+
try {
|
|
8248
|
+
removeDrainCheckpoint(flushingPath);
|
|
8249
|
+
} catch {
|
|
8250
|
+
}
|
|
7928
8251
|
return counts;
|
|
7929
8252
|
}
|
|
7930
|
-
async function drainPath(path, client, binding) {
|
|
8253
|
+
async function drainPath(path, client, binding, onProgress) {
|
|
7931
8254
|
const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
|
|
7932
8255
|
try {
|
|
7933
8256
|
renameSync3(path, tmpPath);
|
|
@@ -7937,7 +8260,19 @@ async function drainPath(path, client, binding) {
|
|
|
7937
8260
|
}
|
|
7938
8261
|
throw err;
|
|
7939
8262
|
}
|
|
7940
|
-
return drainFlushingPath(tmpPath, client, binding);
|
|
8263
|
+
return drainFlushingPath(tmpPath, client, binding, { onProgress });
|
|
8264
|
+
}
|
|
8265
|
+
async function settleDrain(drain) {
|
|
8266
|
+
let progress = { flushed: 0, quarantined: 0 };
|
|
8267
|
+
try {
|
|
8268
|
+
return {
|
|
8269
|
+
counts: await drain((delta) => {
|
|
8270
|
+
progress = addDrainCounts(progress, delta);
|
|
8271
|
+
})
|
|
8272
|
+
};
|
|
8273
|
+
} catch (error) {
|
|
8274
|
+
return { counts: progress, failure: { error } };
|
|
8275
|
+
}
|
|
7941
8276
|
}
|
|
7942
8277
|
function selectRecoverable(files, now, opts = {}) {
|
|
7943
8278
|
const quarantineMs = opts.quarantineMs ?? ORPHAN_QUARANTINE_MS;
|
|
@@ -7954,7 +8289,8 @@ async function recoverOrphans(candidates, options) {
|
|
|
7954
8289
|
flushed: 0,
|
|
7955
8290
|
quarantined: 0,
|
|
7956
8291
|
errors: [],
|
|
7957
|
-
failedBuckets: /* @__PURE__ */ new Set()
|
|
8292
|
+
failedBuckets: /* @__PURE__ */ new Set(),
|
|
8293
|
+
failedDrainFlushed: 0
|
|
7958
8294
|
};
|
|
7959
8295
|
const recoverable = selectRecoverable(candidates, options.now ?? Date.now(), {
|
|
7960
8296
|
ownerPid: options.ownerPid ?? process.pid,
|
|
@@ -7965,28 +8301,41 @@ async function recoverOrphans(candidates, options) {
|
|
|
7965
8301
|
if (summary.failedBuckets.has(file.bucket)) {
|
|
7966
8302
|
continue;
|
|
7967
8303
|
}
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
summary.errors.push(
|
|
8304
|
+
const { counts, failure: failure4 } = await settleDrain((onProgress) => drain(file.path, onProgress));
|
|
8305
|
+
summary.flushed += counts.flushed;
|
|
8306
|
+
summary.quarantined += counts.quarantined;
|
|
8307
|
+
if (failure4) {
|
|
8308
|
+
summary.failedDrainFlushed += counts.flushed;
|
|
8309
|
+
summary.errors.push(failure4.error);
|
|
7974
8310
|
summary.failedBuckets.add(file.bucket);
|
|
7975
8311
|
}
|
|
7976
8312
|
}
|
|
7977
8313
|
return summary;
|
|
7978
8314
|
}
|
|
7979
8315
|
var FlushError = class extends Error {
|
|
8316
|
+
/** Moves the whole sweep acknowledged, across every drain it ran. */
|
|
7980
8317
|
flushed;
|
|
7981
8318
|
quarantined;
|
|
7982
|
-
|
|
8319
|
+
/** Buckets the failed sweep held back without sending, as on success. */
|
|
8320
|
+
retained;
|
|
8321
|
+
/**
|
|
8322
|
+
* Of `flushed`, the Moves acknowledged by the drains that failed, before
|
|
8323
|
+
* they failed. Only this shows the failure itself is advancing: `flushed`
|
|
8324
|
+
* also counts other buckets' drains, so a bucket whose drain fails every
|
|
8325
|
+
* time would read as progress while a sibling bucket kept delivering.
|
|
8326
|
+
*/
|
|
8327
|
+
failedDrainFlushed;
|
|
8328
|
+
constructor(cause, flushed, quarantined, retained = [], failedDrainFlushed = 0) {
|
|
7983
8329
|
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
7984
8330
|
this.name = "FlushError";
|
|
7985
8331
|
this.flushed = flushed;
|
|
7986
8332
|
this.quarantined = quarantined;
|
|
8333
|
+
this.retained = retained;
|
|
8334
|
+
this.failedDrainFlushed = failedDrainFlushed;
|
|
7987
8335
|
}
|
|
7988
8336
|
};
|
|
7989
8337
|
async function flushOnce() {
|
|
8338
|
+
sweepOrphanedDrainProgress();
|
|
7990
8339
|
const orphanCandidates = listFlushing({ sampleBytes: 0 });
|
|
7991
8340
|
const liveBuckets = listBuckets();
|
|
7992
8341
|
if (orphanCandidates.length === 0 && liveBuckets.length === 0) {
|
|
@@ -8003,10 +8352,11 @@ async function flushOnce() {
|
|
|
8003
8352
|
const binding = inspection.binding;
|
|
8004
8353
|
const recovered = await recoverOrphans(
|
|
8005
8354
|
orphanCandidates.filter((file) => inspection.deliverableBuckets.has(file.bucket)),
|
|
8006
|
-
{ drain: (path) => drainFlushingPath(path, client, binding) }
|
|
8355
|
+
{ drain: (path, onProgress) => drainFlushingPath(path, client, binding, { onProgress }) }
|
|
8007
8356
|
);
|
|
8008
8357
|
let total = recovered.flushed;
|
|
8009
8358
|
let quarantined = recovered.quarantined;
|
|
8359
|
+
let failedDrainFlushed = recovered.failedDrainFlushed;
|
|
8010
8360
|
const errors = recovered.errors;
|
|
8011
8361
|
for (const { bucket, path } of liveBuckets) {
|
|
8012
8362
|
if (!inspection.deliverableBuckets.has(bucket)) {
|
|
@@ -8015,16 +8365,24 @@ async function flushOnce() {
|
|
|
8015
8365
|
if (recovered.failedBuckets.has(bucket)) {
|
|
8016
8366
|
continue;
|
|
8017
8367
|
}
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8368
|
+
const { counts, failure: failure4 } = await settleDrain(
|
|
8369
|
+
(onProgress) => drainPath(path, client, binding, onProgress)
|
|
8370
|
+
);
|
|
8371
|
+
total += counts.flushed;
|
|
8372
|
+
quarantined += counts.quarantined;
|
|
8373
|
+
if (failure4) {
|
|
8374
|
+
failedDrainFlushed += counts.flushed;
|
|
8375
|
+
errors.push(failure4.error);
|
|
8024
8376
|
}
|
|
8025
8377
|
}
|
|
8026
8378
|
if (errors.length > 0) {
|
|
8027
|
-
throw new FlushError(
|
|
8379
|
+
throw new FlushError(
|
|
8380
|
+
errors[0],
|
|
8381
|
+
total,
|
|
8382
|
+
quarantined,
|
|
8383
|
+
inspection.retainedBuckets,
|
|
8384
|
+
failedDrainFlushed
|
|
8385
|
+
);
|
|
8028
8386
|
}
|
|
8029
8387
|
return inspection.retainedBuckets.length > 0 ? { flushed: total, quarantined, retained: inspection.retainedBuckets } : { flushed: total, quarantined };
|
|
8030
8388
|
}
|
|
@@ -8054,18 +8412,16 @@ function flush() {
|
|
|
8054
8412
|
return attempt;
|
|
8055
8413
|
}
|
|
8056
8414
|
function shouldFlushPending(stats, now, thresholdMs = OPPORTUNISTIC_FLUSH_AFTER_MS) {
|
|
8057
|
-
if (stats.sampled) {
|
|
8415
|
+
if (stats.sampled || stats.strandedFileCount > 0 && stats.strandedCount === 0) {
|
|
8058
8416
|
return true;
|
|
8059
8417
|
}
|
|
8060
8418
|
return stats.pendingCount > 0 && (stats.oldestPendingAt === void 0 || now - stats.oldestPendingAt > thresholdMs);
|
|
8061
8419
|
}
|
|
8062
|
-
|
|
8420
|
+
function journalNeedsFlush(now = Date.now()) {
|
|
8063
8421
|
try {
|
|
8064
|
-
|
|
8065
|
-
if (shouldFlushPending(stats, Date.now())) {
|
|
8066
|
-
await flush();
|
|
8067
|
-
}
|
|
8422
|
+
return shouldFlushPending(pendingJournalStats(), now);
|
|
8068
8423
|
} catch {
|
|
8424
|
+
return false;
|
|
8069
8425
|
}
|
|
8070
8426
|
}
|
|
8071
8427
|
|
|
@@ -8124,7 +8480,7 @@ function registerMovesCommands(program2) {
|
|
|
8124
8480
|
}
|
|
8125
8481
|
if (stranded.length > 0) {
|
|
8126
8482
|
const moveCount = stranded.reduce((n, f) => n + f.lineCount, 0);
|
|
8127
|
-
const byteCount = stranded.reduce((n, f) => n + f.
|
|
8483
|
+
const byteCount = stranded.reduce((n, f) => n + f.pendingBytes, 0);
|
|
8128
8484
|
const sampled = stranded.some((file) => file.sampled);
|
|
8129
8485
|
const qualifier = sampled ? "at least " : "";
|
|
8130
8486
|
console.log(
|
|
@@ -8135,7 +8491,7 @@ function registerMovesCommands(program2) {
|
|
|
8135
8491
|
const owner = f.pid === void 0 ? "no pid" : `pid ${String(f.pid)}`;
|
|
8136
8492
|
const count = `${String(f.lineCount)}${f.sampled ? "+" : ""}`;
|
|
8137
8493
|
console.log(
|
|
8138
|
-
` ${f.bucket.padEnd(BUCKET_COL_WIDTH)} ${count.padStart(5)} stranded, ${String(f.
|
|
8494
|
+
` ${f.bucket.padEnd(BUCKET_COL_WIDTH)} ${count.padStart(5)} stranded, ${String(f.pendingBytes).padStart(8)} bytes, ${String(ageS)}s ago (${owner})`
|
|
8139
8495
|
);
|
|
8140
8496
|
}
|
|
8141
8497
|
}
|
|
@@ -8333,6 +8689,20 @@ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
|
|
|
8333
8689
|
import { join as join11 } from "path";
|
|
8334
8690
|
var EXIT_INCOMPLETE = 1;
|
|
8335
8691
|
var EXIT_USAGE3 = 2;
|
|
8692
|
+
var SETUP_ORCHESTRATOR_ENV = "PRIM_SETUP_ORCHESTRATOR";
|
|
8693
|
+
var SETUP_DAEMON_DRAINS_ENV = "PRIM_SETUP_DAEMON_DRAINS";
|
|
8694
|
+
function setupStepSpawnOptions(capture, env = process.env, options = {}) {
|
|
8695
|
+
return {
|
|
8696
|
+
env: {
|
|
8697
|
+
...env,
|
|
8698
|
+
[SETUP_ORCHESTRATOR_ENV]: "1",
|
|
8699
|
+
// Unset rather than inherited when this setup starts no daemon.
|
|
8700
|
+
[SETUP_DAEMON_DRAINS_ENV]: options.startsDaemon === true ? "1" : void 0
|
|
8701
|
+
},
|
|
8702
|
+
stdio: capture ? ["inherit", "pipe", "ignore"] : "inherit",
|
|
8703
|
+
encoding: "utf-8"
|
|
8704
|
+
};
|
|
8705
|
+
}
|
|
8336
8706
|
var SESSION_LABELS = {
|
|
8337
8707
|
claude: "Claude Code integration",
|
|
8338
8708
|
codex: "Codex integration",
|
|
@@ -8383,7 +8753,7 @@ function planSetupSteps(opts) {
|
|
|
8383
8753
|
steps.push({
|
|
8384
8754
|
key: "health",
|
|
8385
8755
|
label: "Capture health",
|
|
8386
|
-
args: ["doctor"],
|
|
8756
|
+
args: ["doctor", "--expect-backlog"],
|
|
8387
8757
|
required: true
|
|
8388
8758
|
});
|
|
8389
8759
|
}
|
|
@@ -8483,10 +8853,11 @@ function registerSetupCommand(program2, dependencies = {}) {
|
|
|
8483
8853
|
const self = process.argv[1];
|
|
8484
8854
|
const globals = program2.optsWithGlobals();
|
|
8485
8855
|
const run = dependencies.run ?? ((args, capture = false) => {
|
|
8486
|
-
const r = spawnSync2(
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8856
|
+
const r = spawnSync2(
|
|
8857
|
+
process.execPath,
|
|
8858
|
+
[self, ...args],
|
|
8859
|
+
setupStepSpawnOptions(capture, process.env, { startsDaemon: opts.daemon })
|
|
8860
|
+
);
|
|
8490
8861
|
return { code: r.status ?? 1, stdout: capture ? r.stdout ?? "" : "" };
|
|
8491
8862
|
});
|
|
8492
8863
|
const results = {};
|
|
@@ -9007,28 +9378,233 @@ function registerWelcomeCommand(program2, deps = { getClient }) {
|
|
|
9007
9378
|
});
|
|
9008
9379
|
}
|
|
9009
9380
|
|
|
9381
|
+
// src/daemon/drift-heal.ts
|
|
9382
|
+
var ATTEMPT_MARKER_MODE = 384;
|
|
9383
|
+
var ATTEMPT_MARKER_MAX_BYTES = 1024;
|
|
9384
|
+
var PATH_SEPARATORS = /[\\/]/u;
|
|
9385
|
+
var DAEMON_DRIFT_HEAL_ENV = "PRIM_DAEMON_DRIFT_HEAL";
|
|
9386
|
+
var DAEMON_DRIFT_HEAL_RETRY_MS = 60 * 60 * 1e3;
|
|
9387
|
+
function readAttempt(path) {
|
|
9388
|
+
const file = readBoundedRegularFile(path, ATTEMPT_MARKER_MAX_BYTES);
|
|
9389
|
+
if (!file) return null;
|
|
9390
|
+
try {
|
|
9391
|
+
const value = JSON.parse(file.text);
|
|
9392
|
+
if (typeof value !== "object" || value === null) return null;
|
|
9393
|
+
const attempt = value;
|
|
9394
|
+
return typeof attempt.attemptedAt === "number" && Number.isFinite(attempt.attemptedAt) && typeof attempt.fromVersion === "string" && typeof attempt.toVersion === "string" ? attempt : null;
|
|
9395
|
+
} catch {
|
|
9396
|
+
return null;
|
|
9397
|
+
}
|
|
9398
|
+
}
|
|
9399
|
+
function claimHeld() {
|
|
9400
|
+
throw new Error("another command holds the daemon drift-heal claim");
|
|
9401
|
+
}
|
|
9402
|
+
function claimAttempt(markerPath2, attempt) {
|
|
9403
|
+
const previous = readAttempt(markerPath2);
|
|
9404
|
+
const elapsed = previous ? attempt.attemptedAt - previous.attemptedAt : Number.POSITIVE_INFINITY;
|
|
9405
|
+
if (elapsed >= 0 && elapsed < DAEMON_DRIFT_HEAL_RETRY_MS) return false;
|
|
9406
|
+
atomicWriteFile(markerPath2, `${JSON.stringify(attempt)}
|
|
9407
|
+
`, { mode: ATTEMPT_MARKER_MODE });
|
|
9408
|
+
return true;
|
|
9409
|
+
}
|
|
9410
|
+
function isInstalledPackage(root) {
|
|
9411
|
+
return root?.split(PATH_SEPARATORS).includes("node_modules") === true;
|
|
9412
|
+
}
|
|
9413
|
+
function needsHeal(launcher, toVersion) {
|
|
9414
|
+
const order = compareSemver(launcher.runtimeVersion, toVersion);
|
|
9415
|
+
return order === -1 || order === 0 && !launcher.runnable;
|
|
9416
|
+
}
|
|
9417
|
+
function startDriftHeal(options) {
|
|
9418
|
+
if ((options.platform ?? process.platform) !== "darwin") return false;
|
|
9419
|
+
const env = options.env ?? process.env;
|
|
9420
|
+
if (env[DAEMON_DRIFT_HEAL_ENV] === "0") return false;
|
|
9421
|
+
const root = options.packageRoot === void 0 ? packageRoot() : options.packageRoot;
|
|
9422
|
+
if (!isInstalledPackage(root)) return false;
|
|
9423
|
+
const euid = options.euid ?? process.geteuid?.();
|
|
9424
|
+
if (euid === void 0 || euid === 0) return false;
|
|
9425
|
+
const paths = { env, homeDir: options.homeDir };
|
|
9426
|
+
if (daemonExplicitlyDisabled(paths)) return false;
|
|
9427
|
+
const launcher = (options.selectedLauncher ?? (() => selectedDaemonLauncher(paths)))();
|
|
9428
|
+
if (!launcher || launcher.ownerUid !== euid) return false;
|
|
9429
|
+
const toVersion = options.cliVersion === void 0 ? packageVersion() : options.cliVersion;
|
|
9430
|
+
if (!(toVersion && needsHeal(launcher, toVersion))) return false;
|
|
9431
|
+
if (!apiUrlsMatch(
|
|
9432
|
+
getSiteUrlForEnvironment(launcher.apiUrl),
|
|
9433
|
+
getSiteUrlForEnvironment(env.PRIM_API_URL)
|
|
9434
|
+
)) {
|
|
9435
|
+
return false;
|
|
9436
|
+
}
|
|
9437
|
+
const runsConfigRoot = options.launchAgentRunsConfigRoot ?? (() => launchAgentRunsConfigRoot(paths));
|
|
9438
|
+
if (!runsConfigRoot()) return false;
|
|
9439
|
+
const primEntry = options.primEntry === void 0 ? binFile("prim") : options.primEntry;
|
|
9440
|
+
if (!primEntry) return false;
|
|
9441
|
+
const attempt = {
|
|
9442
|
+
attemptedAt: (options.nowMs ?? Date.now)(),
|
|
9443
|
+
fromVersion: launcher.runtimeVersion,
|
|
9444
|
+
toVersion
|
|
9445
|
+
};
|
|
9446
|
+
const claimed = withFileLockSync(
|
|
9447
|
+
daemonDriftHealClaimPath(paths),
|
|
9448
|
+
() => claimAttempt(daemonDriftHealMarkerPath(paths), attempt),
|
|
9449
|
+
{ timeoutMs: 0, sleep: claimHeld }
|
|
9450
|
+
);
|
|
9451
|
+
if (!claimed) return false;
|
|
9452
|
+
return kickDaemonEnsure({
|
|
9453
|
+
primEntry,
|
|
9454
|
+
nodeEntry: options.nodeEntry,
|
|
9455
|
+
spawnProcess: options.spawnProcess,
|
|
9456
|
+
latestBootstrap: false
|
|
9457
|
+
});
|
|
9458
|
+
}
|
|
9459
|
+
function healDaemonDrift(options = {}) {
|
|
9460
|
+
try {
|
|
9461
|
+
return startDriftHeal(options);
|
|
9462
|
+
} catch {
|
|
9463
|
+
return false;
|
|
9464
|
+
}
|
|
9465
|
+
}
|
|
9466
|
+
|
|
9467
|
+
// src/lib/background-flush.ts
|
|
9468
|
+
import { spawn as spawn3 } from "child_process";
|
|
9469
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
9470
|
+
|
|
9471
|
+
// src/daemon/health.ts
|
|
9472
|
+
import { chmodSync as chmodSync3, existsSync as existsSync11, mkdirSync as mkdirSync7, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
9473
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
9474
|
+
var HEARTBEAT_FRESH_MS = 9e4;
|
|
9475
|
+
var DAEMON_HEALTH_PATH = join12(primConfigDirectory(), "daemon-health.json");
|
|
9476
|
+
|
|
9477
|
+
// src/lib/background-flush.ts
|
|
9478
|
+
function record2(value) {
|
|
9479
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
9480
|
+
}
|
|
9481
|
+
function daemonOwnsDrain(options = {}) {
|
|
9482
|
+
try {
|
|
9483
|
+
const expectedVersion = options.expectedVersion === void 0 ? packageVersion() : options.expectedVersion;
|
|
9484
|
+
if (!expectedVersion) return false;
|
|
9485
|
+
const health = record2(
|
|
9486
|
+
JSON.parse(readFileSync9(options.healthPath ?? DAEMON_HEALTH_PATH, "utf8"))
|
|
9487
|
+
);
|
|
9488
|
+
const heartbeat = record2(health?.heartbeat);
|
|
9489
|
+
const ingestion = record2(health?.ingestion);
|
|
9490
|
+
if (!(health && heartbeat && ingestion)) return false;
|
|
9491
|
+
if (health.schemaVersion !== 1 || health.version !== expectedVersion) return false;
|
|
9492
|
+
const daemonSiteUrl = health.siteUrl;
|
|
9493
|
+
if (typeof daemonSiteUrl !== "string" || envSlug(daemonSiteUrl) !== envSlug(options.siteUrl ?? getSiteUrl())) {
|
|
9494
|
+
return false;
|
|
9495
|
+
}
|
|
9496
|
+
if (health.needsReauth === true) return false;
|
|
9497
|
+
const pid = health.pid;
|
|
9498
|
+
if (typeof pid !== "number" || !(options.isAlive ?? processIsAlive)(pid)) return false;
|
|
9499
|
+
const lastSuccessAt = heartbeat.lastSuccessAt;
|
|
9500
|
+
const now = options.now ?? Date.now();
|
|
9501
|
+
return heartbeat.consecutiveFailures === 0 && typeof lastSuccessAt === "number" && lastSuccessAt <= now && now - lastSuccessAt < HEARTBEAT_FRESH_MS && ingestion.consecutiveFailures === 0;
|
|
9502
|
+
} catch {
|
|
9503
|
+
return false;
|
|
9504
|
+
}
|
|
9505
|
+
}
|
|
9506
|
+
function startBackgroundFlush(options = {}) {
|
|
9507
|
+
try {
|
|
9508
|
+
if ((options.daemonOwnsDrain ?? daemonOwnsDrain)()) return false;
|
|
9509
|
+
if (!(options.needsFlush ?? journalNeedsFlush)()) return false;
|
|
9510
|
+
const primEntry = options.primEntry === void 0 ? binFile("prim") : options.primEntry;
|
|
9511
|
+
if (!primEntry) return false;
|
|
9512
|
+
const child = (options.spawnProcess ?? spawn3)(
|
|
9513
|
+
options.nodeEntry ?? process.execPath,
|
|
9514
|
+
[primEntry, "moves", "flush"],
|
|
9515
|
+
// Ignored stdio matters as much as detaching: a child holding inherited
|
|
9516
|
+
// pipes would keep a caller that captures them (spawnSync) waiting.
|
|
9517
|
+
// windowsHide keeps the detached child from opening a console window.
|
|
9518
|
+
// The child is unattended even when its parent is not: `doctor` or
|
|
9519
|
+
// `statusline` must not reach the daemon drift heal through its drain.
|
|
9520
|
+
{ detached: true, stdio: "ignore", windowsHide: true, env: unattendedEnv() }
|
|
9521
|
+
);
|
|
9522
|
+
child.once("error", () => {
|
|
9523
|
+
});
|
|
9524
|
+
child.unref();
|
|
9525
|
+
return true;
|
|
9526
|
+
} catch {
|
|
9527
|
+
return false;
|
|
9528
|
+
}
|
|
9529
|
+
}
|
|
9530
|
+
|
|
9010
9531
|
// src/lib/startup-background.ts
|
|
9011
9532
|
var ROOT_OPTIONS = /* @__PURE__ */ new Set(["-y", "--yes", "--non-interactive"]);
|
|
9533
|
+
var HELP_OPTIONS = /* @__PURE__ */ new Set(["-h", "--help"]);
|
|
9534
|
+
var DAEMON_HEAL_COMMANDS = /* @__PURE__ */ new Map([
|
|
9535
|
+
// Credentials; `auth login` is how a person returns to a daemon held for re-auth.
|
|
9536
|
+
["auth", /* @__PURE__ */ new Set(["api-keys", "login", "set-token", "status"])],
|
|
9537
|
+
// Reading and curating the decision graph: the everyday attended surface.
|
|
9538
|
+
[
|
|
9539
|
+
"decisions",
|
|
9540
|
+
/* @__PURE__ */ new Set([
|
|
9541
|
+
"cascade",
|
|
9542
|
+
"check",
|
|
9543
|
+
"confirm",
|
|
9544
|
+
"create",
|
|
9545
|
+
"demote",
|
|
9546
|
+
"link",
|
|
9547
|
+
"promote",
|
|
9548
|
+
"publish",
|
|
9549
|
+
"ratify",
|
|
9550
|
+
"recent",
|
|
9551
|
+
"repairs",
|
|
9552
|
+
"rescope",
|
|
9553
|
+
"restore",
|
|
9554
|
+
"show",
|
|
9555
|
+
"supersede",
|
|
9556
|
+
"unlink",
|
|
9557
|
+
"withdraw"
|
|
9558
|
+
])
|
|
9559
|
+
],
|
|
9560
|
+
["reconcile", "any"],
|
|
9561
|
+
// Repository connection, activation, and journal plumbing a person steers.
|
|
9562
|
+
// Hook-side `moves flush` drains are unattended and never reach this table.
|
|
9563
|
+
["github", /* @__PURE__ */ new Set(["connect"])],
|
|
9564
|
+
["enable", "any"],
|
|
9565
|
+
["moves", /* @__PURE__ */ new Set(["bind", "drop", "flush", "status", "tail"])],
|
|
9566
|
+
["session", /* @__PURE__ */ new Set(["drop", "list", "start"])],
|
|
9567
|
+
// Installing or inspecting an integration, never removing one.
|
|
9568
|
+
["claude", /* @__PURE__ */ new Set(["install", "preauth", "status"])],
|
|
9569
|
+
["codex", /* @__PURE__ */ new Set(["install", "status"])],
|
|
9570
|
+
["cursor", /* @__PURE__ */ new Set(["install", "status"])],
|
|
9571
|
+
["hermes", /* @__PURE__ */ new Set(["install", "status"])],
|
|
9572
|
+
["hooks", /* @__PURE__ */ new Set(["install"])],
|
|
9573
|
+
["skill", /* @__PURE__ */ new Set(["install", "status"])],
|
|
9574
|
+
["welcome", "any"]
|
|
9575
|
+
]);
|
|
9012
9576
|
function commandArguments(argv) {
|
|
9013
9577
|
let index = 0;
|
|
9014
9578
|
while (ROOT_OPTIONS.has(argv[index] ?? "")) index += 1;
|
|
9015
9579
|
return argv.slice(index);
|
|
9016
9580
|
}
|
|
9581
|
+
function mayHealDaemon(argv, env) {
|
|
9582
|
+
if (isUnattended(env) || env[SETUP_ORCHESTRATOR_ENV] === "1") return false;
|
|
9583
|
+
const args = commandArguments(argv);
|
|
9584
|
+
if (args.some((arg) => HELP_OPTIONS.has(arg))) return false;
|
|
9585
|
+
const [command, subcommand] = args;
|
|
9586
|
+
const allowed = command === void 0 ? void 0 : DAEMON_HEAL_COMMANDS.get(command);
|
|
9587
|
+
if (allowed === void 0) return false;
|
|
9588
|
+
return allowed === "any" || subcommand !== void 0 && allowed.has(subcommand);
|
|
9589
|
+
}
|
|
9017
9590
|
function runStartupBackgroundWork(argv, env, work) {
|
|
9018
|
-
const
|
|
9591
|
+
const args = commandArguments(argv);
|
|
9592
|
+
const [command, subcommand] = args;
|
|
9019
9593
|
const uninstall = command === "uninstall" || env[UNINSTALL_ORCHESTRATOR_ENV] === "1";
|
|
9020
9594
|
if (uninstall) return;
|
|
9021
9595
|
work.notify();
|
|
9022
|
-
|
|
9023
|
-
|
|
9596
|
+
const setupDaemonDrains = command === "setup" && !args.includes("--no-daemon") || env[SETUP_DAEMON_DRAINS_ENV] === "1";
|
|
9597
|
+
if (!setupDaemonDrains && (command !== "moves" || subcommand !== "flush")) work.flush();
|
|
9598
|
+
if (mayHealDaemon(argv, env)) work.healDaemon();
|
|
9024
9599
|
}
|
|
9025
9600
|
|
|
9026
9601
|
// src/index.ts
|
|
9027
|
-
var __dirname =
|
|
9028
|
-
var pkg = JSON.parse(
|
|
9602
|
+
var __dirname = dirname7(fileURLToPath2(import.meta.url));
|
|
9603
|
+
var pkg = JSON.parse(readFileSync10(resolve4(__dirname, "../package.json"), "utf-8"));
|
|
9029
9604
|
runStartupBackgroundWork(process.argv.slice(2), process.env, {
|
|
9030
9605
|
notify: () => updateNotifier({ pkg }).notify(),
|
|
9031
|
-
flush: () =>
|
|
9606
|
+
flush: () => startBackgroundFlush(),
|
|
9607
|
+
healDaemon: () => healDaemonDrift()
|
|
9032
9608
|
});
|
|
9033
9609
|
var program = new Command();
|
|
9034
9610
|
program.name("prim").description("CLI for Primitive's decision graph").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
|