harnery 0.45.0 → 0.45.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commander.js +0 -1
- package/dist/commands/files.d.ts +2 -2
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +4 -7
- package/dist/commands/servers.d.ts.map +1 -1
- package/dist/commands/servers.js +17 -11
- package/dist/commands/tunnel.d.ts +0 -15
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +153 -453
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +6 -45
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +0 -18
- package/dist/core/agents/live-authority-v3.d.ts +0 -1
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +0 -11
- package/dist/core/agents/live-lifecycle-v3.d.ts +2 -4
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/live-lifecycle-v3.js +7 -28
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +14 -25
- package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-writer.js +0 -5
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +2 -7
- package/dist/core/agents/state/proc-start.d.ts +20 -31
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +37 -70
- package/dist/core/agents/state/service-process.d.ts +61 -0
- package/dist/core/agents/state/service-process.d.ts.map +1 -0
- package/dist/core/agents/state/service-process.js +143 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +70 -180
- package/dist/core/artifacts/activity.d.ts +4 -1
- package/dist/core/artifacts/activity.d.ts.map +1 -1
- package/dist/core/artifacts/activity.js +8 -2
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
- package/dist/core/artifacts/delivery-card.js +1 -2
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +38 -12
- package/dist/core/context/index.d.ts +6 -4
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +194 -256
- package/dist/core/context/observation.d.ts +9 -9
- package/dist/core/context/observation.d.ts.map +1 -1
- package/dist/core/context/observation.js +37 -6
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +0 -29
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +0 -1
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +0 -7
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +64 -77
- package/dist/core/events/v3/runtime-build.d.ts +14 -0
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +44 -0
- package/dist/core/governor/index.d.ts +2 -1
- package/dist/core/governor/index.d.ts.map +1 -1
- package/dist/core/governor/index.js +2 -1
- package/dist/core/governor/service-read.d.ts +3 -10
- package/dist/core/governor/service-read.d.ts.map +1 -1
- package/dist/core/governor/service-read.js +10 -94
- package/dist/core/governor/service.d.ts +3 -7
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +17 -182
- package/dist/core/hooks/cli.js +78 -48
- package/dist/core/hooks/resolve/owner.js +2 -2
- package/dist/core/resources/sampler.js +2 -2
- package/dist/core/resources/service-status.d.ts +2 -14
- package/dist/core/resources/service-status.d.ts.map +1 -1
- package/dist/core/resources/service-status.js +3 -23
- package/dist/core/resources/service.d.ts +3 -7
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +12 -101
- package/dist/core/resources/status.d.ts.map +1 -1
- package/dist/core/resources/status.js +3 -2
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +9 -2
- package/dist/core/servers/index.d.ts +11 -10
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +82 -192
- package/dist/core/servers/tunnels.d.ts +3 -5
- package/dist/core/servers/tunnels.d.ts.map +1 -1
- package/dist/core/servers/tunnels.js +2 -45
- package/dist/core/storage/durable-history.js +56 -22
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +73 -17
- package/dist/core/supervisor/service.d.ts +4 -7
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +23 -122
- package/dist/core/supervisor/services.js +2 -2
- package/dist/core/supervisor/status.d.ts +3 -17
- package/dist/core/supervisor/status.d.ts.map +1 -1
- package/dist/core/supervisor/status.js +7 -70
- package/dist/core/work/state.js +7 -7
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +23 -66
- package/dist/core/workflow/index.d.ts +3 -4
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -2
- package/dist/core/workflow/live-session-v3.d.ts +0 -6
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +5 -126
- package/dist/core/workflow/proof.d.ts +4 -4
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +34 -86
- package/dist/core/workflow/resume-lock.d.ts +17 -0
- package/dist/core/workflow/resume-lock.d.ts.map +1 -0
- package/dist/core/workflow/resume-lock.js +158 -0
- package/dist/core/workflow/run-state.d.ts +0 -21
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +3 -174
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +0 -19
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +0 -13
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +0 -19
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +0 -14
- package/dist/core/workflow/types.d.ts +11 -54
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.d.ts +4 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +1 -1
- package/dist/core/workflow/workspaces/local-git.js +91 -210
- package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
- package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/ownership.js +3 -130
- package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/reclaim.js +0 -4
- package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +1 -1
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +1 -4
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +0 -4
- package/dist/lib/tunnel/state.d.ts +6 -13
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +12 -58
- package/package.json +1 -6
- package/src/commander.ts +0 -10
- package/src/commands/files.ts +5 -13
- package/src/commands/servers.ts +23 -12
- package/src/commands/tunnel.ts +166 -533
- package/src/commands/workflow.ts +8 -58
- package/src/core/agents/coord-client.ts +0 -20
- package/src/core/agents/live-authority-v3.ts +0 -17
- package/src/core/agents/live-lifecycle-v3.ts +14 -52
- package/src/core/agents/state/heartbeat-writer.ts +12 -29
- package/src/core/agents/state/live-coordination-writer.ts +0 -29
- package/src/core/agents/state/pidmap.ts +2 -15
- package/src/core/agents/state/proc-start.ts +41 -69
- package/src/core/agents/state/service-process.ts +217 -0
- package/src/core/agents/state/stale-sweep.ts +82 -247
- package/src/core/artifacts/activity.ts +9 -2
- package/src/core/artifacts/delivery-card.ts +1 -7
- package/src/core/artifacts/index.ts +45 -12
- package/src/core/context/index.ts +207 -272
- package/src/core/context/observation.ts +40 -9
- package/src/core/events/v3/authority-outbox.ts +0 -56
- package/src/core/events/v3/producers/coordination-recorder.ts +0 -15
- package/src/core/events/v3/producers/recorder.ts +68 -94
- package/src/core/events/v3/runtime-build.ts +62 -0
- package/src/core/governor/index.ts +1 -2
- package/src/core/governor/service-read.ts +15 -114
- package/src/core/governor/service.ts +36 -220
- package/src/core/hooks/cli.ts +85 -47
- package/src/core/hooks/resolve/owner.ts +2 -2
- package/src/core/resources/sampler.ts +3 -2
- package/src/core/resources/service-status.ts +6 -35
- package/src/core/resources/service.ts +23 -133
- package/src/core/resources/status.ts +3 -2
- package/src/core/semantic/service.ts +8 -2
- package/src/core/servers/index.ts +86 -228
- package/src/core/servers/tunnels.ts +3 -55
- package/src/core/storage/durable-history.ts +52 -20
- package/src/core/storage/segments.ts +79 -17
- package/src/core/supervisor/service.ts +33 -155
- package/src/core/supervisor/services.ts +2 -2
- package/src/core/supervisor/status.ts +10 -85
- package/src/core/work/state.ts +7 -7
- package/src/core/workflow/engine.ts +22 -74
- package/src/core/workflow/index.ts +2 -13
- package/src/core/workflow/live-session-v3.ts +5 -146
- package/src/core/workflow/proof.ts +40 -104
- package/src/core/workflow/resume-lock.ts +179 -0
- package/src/core/workflow/run-state.ts +3 -207
- package/src/core/workflow/spawn-claude.ts +0 -18
- package/src/core/workflow/spawn-codex.ts +0 -13
- package/src/core/workflow/spawn-cursor.ts +0 -18
- package/src/core/workflow/spawn-opencode.ts +0 -14
- package/src/core/workflow/types.ts +11 -41
- package/src/core/workflow/workspaces/leases.ts +1 -1
- package/src/core/workflow/workspaces/local-git.ts +120 -239
- package/src/core/workflow/workspaces/ownership.ts +3 -158
- package/src/core/workflow/workspaces/reclaim.ts +3 -9
- package/src/core/workflow/workspaces/recovery.ts +1 -1
- package/src/core/workflow/workspaces/state.ts +1 -5
- package/src/lib/identities/assume.ts +0 -4
- package/src/lib/tunnel/state.ts +15 -70
- package/dist/commands/turn-rollback.d.ts +0 -5
- package/dist/commands/turn-rollback.d.ts.map +0 -1
- package/dist/commands/turn-rollback.js +0 -76
- package/dist/core/agents/state/process-handle.d.ts +0 -26
- package/dist/core/agents/state/process-handle.d.ts.map +0 -1
- package/dist/core/agents/state/process-handle.js +0 -121
- package/dist/core/agents/state/unchanged-file.d.ts +0 -44
- package/dist/core/agents/state/unchanged-file.d.ts.map +0 -1
- package/dist/core/agents/state/unchanged-file.js +0 -390
- package/dist/core/events/v3/generation-custody.d.ts +0 -5
- package/dist/core/events/v3/generation-custody.d.ts.map +0 -1
- package/dist/core/events/v3/generation-custody.js +0 -50
- package/dist/core/turn-rollback/content.d.ts +0 -53
- package/dist/core/turn-rollback/content.d.ts.map +0 -1
- package/dist/core/turn-rollback/content.js +0 -396
- package/dist/core/turn-rollback/index.d.ts +0 -72
- package/dist/core/turn-rollback/index.d.ts.map +0 -1
- package/dist/core/turn-rollback/index.js +0 -443
- package/dist/core/workflow/failure-evidence.d.ts +0 -16
- package/dist/core/workflow/failure-evidence.d.ts.map +0 -1
- package/dist/core/workflow/failure-evidence.js +0 -114
- package/dist/core/workflow/recovery-audit.d.ts +0 -30
- package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
- package/dist/core/workflow/recovery-audit.js +0 -116
- package/src/commands/turn-rollback.ts +0 -111
- package/src/core/agents/state/process-handle.ts +0 -165
- package/src/core/agents/state/unchanged-file.ts +0 -456
- package/src/core/events/v3/generation-custody.ts +0 -59
- package/src/core/turn-rollback/content.ts +0 -499
- package/src/core/turn-rollback/index.ts +0 -580
- package/src/core/workflow/failure-evidence.ts +0 -141
- package/src/core/workflow/recovery-audit.ts +0 -181
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One owner check for every singleton background service (governor, local
|
|
3
|
+
* supervisor, legacy resource observer): is the recorded process still the
|
|
4
|
+
* run that wrote the record, and how is a stop requested?
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import {
|
|
8
|
+
closeSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
openSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { hostname } from "node:os";
|
|
17
|
+
import { dirname } from "node:path";
|
|
18
|
+
import { writePrivateJsonAtomic } from "../../storage/atomic-json.ts";
|
|
19
|
+
import { stateDirMode, stateFileMode } from "../../storage/modes.ts";
|
|
20
|
+
import {
|
|
21
|
+
type ProcessGenerationSignalDependencies,
|
|
22
|
+
type ProcessGenerationVerdict,
|
|
23
|
+
processStartToken,
|
|
24
|
+
signalIfSameGeneration,
|
|
25
|
+
verifyProcessGeneration,
|
|
26
|
+
} from "./proc-start.ts";
|
|
27
|
+
|
|
28
|
+
export interface ServiceProcessDependencies extends ProcessGenerationSignalDependencies {
|
|
29
|
+
/** Throws ESRCH, or returns false, when the process is gone. */
|
|
30
|
+
probePid?: (pid: number) => unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ServiceLiveness = "running" | "stopped" | "unknown";
|
|
34
|
+
|
|
35
|
+
export interface ServiceProcessOwner {
|
|
36
|
+
pid: number;
|
|
37
|
+
host: string;
|
|
38
|
+
start_token?: string;
|
|
39
|
+
state?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A stale heartbeat does not prove that a process has exited. Another host, a
|
|
44
|
+
* probe error other than ESRCH, or an unverifiable token answers unknown. A
|
|
45
|
+
* recorded `stopped` state counts only while the generation still matches.
|
|
46
|
+
*/
|
|
47
|
+
export function observeServiceProcess(
|
|
48
|
+
owner: ServiceProcessOwner,
|
|
49
|
+
dependencies: ServiceProcessDependencies = {},
|
|
50
|
+
): { generation: ProcessGenerationVerdict; liveness: ServiceLiveness } {
|
|
51
|
+
if (owner.host !== hostname()) return { generation: "unknown", liveness: "unknown" };
|
|
52
|
+
try {
|
|
53
|
+
if ((dependencies.probePid ?? ((pid) => process.kill(pid, 0)))(owner.pid) === false)
|
|
54
|
+
return { generation: "unknown", liveness: "stopped" };
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return {
|
|
57
|
+
generation: "unknown",
|
|
58
|
+
liveness: (error as NodeJS.ErrnoException).code === "ESRCH" ? "stopped" : "unknown",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const generation = verifyProcessGeneration(owner.pid, owner.start_token, dependencies.readToken);
|
|
62
|
+
return {
|
|
63
|
+
generation,
|
|
64
|
+
liveness:
|
|
65
|
+
generation === "same"
|
|
66
|
+
? owner.state === "stopped"
|
|
67
|
+
? "stopped"
|
|
68
|
+
: "running"
|
|
69
|
+
: generation === "different"
|
|
70
|
+
? "stopped"
|
|
71
|
+
: "unknown",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Refuse a start while another owner runs or cannot be identified. */
|
|
76
|
+
export function serviceStartRefusal(
|
|
77
|
+
label: string,
|
|
78
|
+
stopCommand: string,
|
|
79
|
+
pid: number | undefined,
|
|
80
|
+
liveness: ServiceLiveness,
|
|
81
|
+
): Error {
|
|
82
|
+
return new Error(
|
|
83
|
+
liveness === "running"
|
|
84
|
+
? `${label} is already running under pid ${pid}`
|
|
85
|
+
: `${label} process generation is unknown${pid ? ` for pid ${pid}` : ""}; run \`${stopCommand}\` to write a durable stop request before starting it again`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ServiceLease {
|
|
90
|
+
pid: number;
|
|
91
|
+
start_token?: string;
|
|
92
|
+
host: string;
|
|
93
|
+
nonce: string;
|
|
94
|
+
created_at?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function readServiceLease(path: string): ServiceLease | undefined {
|
|
98
|
+
try {
|
|
99
|
+
const value = JSON.parse(readFileSync(path, "utf8")) as Partial<ServiceLease>;
|
|
100
|
+
if (
|
|
101
|
+
!Number.isSafeInteger(value.pid) ||
|
|
102
|
+
(value.pid ?? 0) <= 0 ||
|
|
103
|
+
typeof value.host !== "string" ||
|
|
104
|
+
!value.host ||
|
|
105
|
+
typeof value.nonce !== "string" ||
|
|
106
|
+
!value.nonce
|
|
107
|
+
)
|
|
108
|
+
return undefined;
|
|
109
|
+
return {
|
|
110
|
+
...value,
|
|
111
|
+
start_token: typeof value.start_token === "string" ? value.start_token : undefined,
|
|
112
|
+
} as ServiceLease;
|
|
113
|
+
} catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** An absent lease is stopped; an unreadable one cannot be identified. */
|
|
119
|
+
export function serviceLeaseOwner(path: string): {
|
|
120
|
+
lease?: ServiceLease;
|
|
121
|
+
liveness: ServiceLiveness;
|
|
122
|
+
} {
|
|
123
|
+
if (!existsSync(path)) return { liveness: "stopped" };
|
|
124
|
+
const lease = readServiceLease(path);
|
|
125
|
+
return { lease, liveness: lease ? observeServiceProcess(lease).liveness : "unknown" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Take the singleton lease with an exclusive create. A lease whose owner is
|
|
130
|
+
* proven stopped is replaced; any other owner refuses the start.
|
|
131
|
+
*/
|
|
132
|
+
export function acquireServiceLease(
|
|
133
|
+
path: string,
|
|
134
|
+
refuse: (pid: number | undefined, liveness: ServiceLiveness) => Error,
|
|
135
|
+
fields: Record<string, unknown> = {},
|
|
136
|
+
): { nonce: string; release: () => void } {
|
|
137
|
+
mkdirSync(dirname(path), { recursive: true, mode: stateDirMode() });
|
|
138
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
139
|
+
const startToken = processStartToken(process.pid);
|
|
140
|
+
const lease = {
|
|
141
|
+
...fields,
|
|
142
|
+
pid: process.pid,
|
|
143
|
+
...(startToken ? { start_token: startToken } : {}),
|
|
144
|
+
host: hostname(),
|
|
145
|
+
nonce: randomUUID(),
|
|
146
|
+
created_at: new Date().toISOString(),
|
|
147
|
+
};
|
|
148
|
+
try {
|
|
149
|
+
const fd = openSync(path, "wx", stateFileMode());
|
|
150
|
+
try {
|
|
151
|
+
writeFileSync(fd, `${JSON.stringify(lease)}\n`, "utf8");
|
|
152
|
+
} finally {
|
|
153
|
+
closeSync(fd);
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
nonce: lease.nonce,
|
|
157
|
+
release: () => {
|
|
158
|
+
if (readServiceLease(path)?.nonce !== lease.nonce) return;
|
|
159
|
+
try {
|
|
160
|
+
unlinkSync(path);
|
|
161
|
+
} catch {
|
|
162
|
+
// A missing lease is already released.
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
168
|
+
const owner = serviceLeaseOwner(path);
|
|
169
|
+
if (owner.liveness !== "stopped") throw refuse(owner.lease?.pid, owner.liveness);
|
|
170
|
+
try {
|
|
171
|
+
unlinkSync(path);
|
|
172
|
+
} catch {
|
|
173
|
+
// A concurrent starter may have replaced or removed the stale lease.
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
throw new Error(`could not acquire the singleton lease at ${path}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface ServiceStopFields {
|
|
181
|
+
signal_sent: boolean;
|
|
182
|
+
signal_verdict: ProcessGenerationVerdict;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Write the durable stop request, then SIGTERM the owner `status` observed as
|
|
187
|
+
* running on this host, after a fresh generation check. The request stays for
|
|
188
|
+
* the daemon either way; `read` reports the state afterward.
|
|
189
|
+
*/
|
|
190
|
+
export function requestServiceStop<
|
|
191
|
+
S extends {
|
|
192
|
+
liveness: ServiceLiveness;
|
|
193
|
+
generation: ProcessGenerationVerdict;
|
|
194
|
+
record?: ServiceProcessOwner;
|
|
195
|
+
},
|
|
196
|
+
>(
|
|
197
|
+
status: S,
|
|
198
|
+
read: () => S,
|
|
199
|
+
stopPath: string,
|
|
200
|
+
dependencies: ServiceProcessDependencies = {},
|
|
201
|
+
options: { signal?: boolean } = {},
|
|
202
|
+
): S & ServiceStopFields {
|
|
203
|
+
writePrivateJsonAtomic(stopPath, {
|
|
204
|
+
requested_at: new Date().toISOString(),
|
|
205
|
+
requested_by_pid: process.pid,
|
|
206
|
+
});
|
|
207
|
+
const signal =
|
|
208
|
+
options.signal !== false && status.liveness === "running" && status.record?.host === hostname()
|
|
209
|
+
? signalIfSameGeneration(
|
|
210
|
+
status.record.pid,
|
|
211
|
+
status.record.start_token,
|
|
212
|
+
"SIGTERM",
|
|
213
|
+
dependencies,
|
|
214
|
+
)
|
|
215
|
+
: { sent: false, verdict: status.generation };
|
|
216
|
+
return { ...read(), signal_sent: signal.sent, signal_verdict: signal.verdict };
|
|
217
|
+
}
|
|
@@ -10,21 +10,12 @@
|
|
|
10
10
|
* `coord.freshness_seconds` (see `coordFreshnessSeconds`).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync,
|
|
14
|
-
import {
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
15
|
import { coordFreshnessSeconds } from "../../config.ts";
|
|
16
16
|
import { type EventAdapterIdV3, eventAdapterIdV3FromPlatform } from "../../events/v3/adapter-id.ts";
|
|
17
|
-
import { withGenerationCustodyV3 } from "../../events/v3/generation-custody.ts";
|
|
18
|
-
import { liveInstanceIdV3 } from "../../events/v3/live-routing.ts";
|
|
19
17
|
import { writeProducerDiagnosticV3 } from "../../events/v3/producers/intake.ts";
|
|
20
18
|
import { recordLiveSweepObservationV3 } from "../live-lifecycle-v3.ts";
|
|
21
|
-
import {
|
|
22
|
-
captureFileRevision,
|
|
23
|
-
type DeleteFileResult,
|
|
24
|
-
deleteFileIfUnchanged,
|
|
25
|
-
type FileRecoveryResult,
|
|
26
|
-
recoverInterruptedFileDeletions,
|
|
27
|
-
} from "./unchanged-file.ts";
|
|
28
19
|
|
|
29
20
|
/** platform → adapter, for the swept-event envelope (mirrors heartbeat-writer's adapterOf). */
|
|
30
21
|
/**
|
|
@@ -41,7 +32,6 @@ function emitSwept(
|
|
|
41
32
|
instanceId: string,
|
|
42
33
|
adapter: EventAdapterIdV3,
|
|
43
34
|
sessionId: string,
|
|
44
|
-
expectedGenerationId: `gen_${string}` | null,
|
|
45
35
|
reason: "stale" | "unparseable" | "missing_ts",
|
|
46
36
|
ageSecs?: number,
|
|
47
37
|
): boolean {
|
|
@@ -51,7 +41,6 @@ function emitSwept(
|
|
|
51
41
|
owner: instanceId,
|
|
52
42
|
nativeSessionId: sessionId,
|
|
53
43
|
adapter,
|
|
54
|
-
expectedGenerationId,
|
|
55
44
|
observation:
|
|
56
45
|
reason === "stale"
|
|
57
46
|
? "stale_heartbeat"
|
|
@@ -76,73 +65,13 @@ function emitSwept(
|
|
|
76
65
|
}
|
|
77
66
|
}
|
|
78
67
|
|
|
79
|
-
/**
|
|
80
|
-
function
|
|
81
|
-
coordRoot: string,
|
|
82
|
-
activeDir: string,
|
|
83
|
-
owner: string,
|
|
84
|
-
uncertainOwners: Set<string>,
|
|
85
|
-
): boolean {
|
|
86
|
-
if (uncertainOwners.has("*") || uncertainOwners.has(owner)) return true;
|
|
87
|
-
if (!/^[A-Za-z0-9_-]{1,128}$/.test(owner)) return true;
|
|
88
|
-
const heartbeat = join(activeDir, `${owner}.json`);
|
|
89
|
-
try {
|
|
90
|
-
lstatSync(heartbeat);
|
|
91
|
-
return true;
|
|
92
|
-
} catch (error) {
|
|
93
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") return true;
|
|
94
|
-
}
|
|
68
|
+
/** File mtime in epoch-seconds, or +Infinity if it can't be read (treat as fresh → don't reap). */
|
|
69
|
+
function mtimeSecs(path: string): number {
|
|
95
70
|
try {
|
|
96
|
-
|
|
97
|
-
} catch
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
// Callers hold this owner's custody. A sweeper that died after the initial
|
|
101
|
-
// directory scan may have left its heartbeat hidden under a private pin.
|
|
102
|
-
for (const recovery of recoverInterruptedFileDeletions(
|
|
103
|
-
activeDir,
|
|
104
|
-
(source) => source === heartbeat,
|
|
105
|
-
)) {
|
|
106
|
-
if (recovery.sourcePath && recovery.sourcePath !== heartbeat) continue;
|
|
107
|
-
if (recovery.state !== "restored") {
|
|
108
|
-
uncertainOwners.add(recovery.sourcePath ? owner : "*");
|
|
109
|
-
recordCleanupFailure(coordRoot, recovery.sourcePath ?? activeDir, recovery);
|
|
110
|
-
}
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function recordCleanupFailure(
|
|
117
|
-
coordRoot: string,
|
|
118
|
-
sourcePath: string,
|
|
119
|
-
result: DeleteFileResult | FileRecoveryResult,
|
|
120
|
-
): void {
|
|
121
|
-
if (
|
|
122
|
-
result.state !== "error" &&
|
|
123
|
-
result.state !== "recovery-required" &&
|
|
124
|
-
result.state !== "retained"
|
|
125
|
-
) {
|
|
126
|
-
return;
|
|
71
|
+
return Math.floor(statSync(path).mtimeMs / 1000);
|
|
72
|
+
} catch {
|
|
73
|
+
return Number.POSITIVE_INFINITY;
|
|
127
74
|
}
|
|
128
|
-
writeProducerDiagnosticV3(coordRoot, "heartbeat_sweep_unrecorded", {
|
|
129
|
-
signal: "cache.cleanup",
|
|
130
|
-
state: "cache_cleanup_pending",
|
|
131
|
-
reason: `${result.state}:${result.error ?? "file_cleanup_pending"}; source=${sourcePath}; recovery=${result.recoveryPath ?? "unavailable"}`,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function recordHeartbeatCleanupResult(
|
|
136
|
-
coordRoot: string,
|
|
137
|
-
sourcePath: string,
|
|
138
|
-
owner: string,
|
|
139
|
-
result: DeleteFileResult,
|
|
140
|
-
uncertainOwners: Set<string>,
|
|
141
|
-
): void {
|
|
142
|
-
if (result.state === "recovery-required" || result.state === "error") {
|
|
143
|
-
uncertainOwners.add(owner);
|
|
144
|
-
}
|
|
145
|
-
recordCleanupFailure(coordRoot, sourcePath, result);
|
|
146
75
|
}
|
|
147
76
|
|
|
148
77
|
export function staleSweep(coordRoot: string): {
|
|
@@ -176,171 +105,98 @@ export function staleSweep(coordRoot: string): {
|
|
|
176
105
|
// lifecycle.sweep_observed; a terminal or unavailable generation gets a
|
|
177
106
|
// heartbeat_sweep_unrecorded producer diagnostic without being resurrected.
|
|
178
107
|
const liveInstanceIds = new Set<string>();
|
|
179
|
-
const
|
|
180
|
-
const d = resolve(coordRoot, ".harnery", "active");
|
|
181
|
-
for (const recovery of existsSync(d)
|
|
182
|
-
? recoverInterruptedFileDeletions(
|
|
183
|
-
d,
|
|
184
|
-
(source) => dirname(source) === d && /^[A-Za-z0-9_-]{1,128}\.json$/.test(basename(source)),
|
|
185
|
-
)
|
|
186
|
-
: []) {
|
|
187
|
-
if (recovery.state !== "restored") {
|
|
188
|
-
uncertainOwners.add(recovery.sourcePath ? basename(recovery.sourcePath, ".json") : "*");
|
|
189
|
-
recordCleanupFailure(coordRoot, recovery.sourcePath ?? d, recovery);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
108
|
+
const d = join(coordRoot, ".harnery", "active");
|
|
192
109
|
if (existsSync(d)) {
|
|
193
110
|
for (const f of readdirSync(d)) {
|
|
194
111
|
if (!f.endsWith(".json")) continue;
|
|
195
112
|
const path = join(d, f);
|
|
196
113
|
const idFromFile = f.replace(/\.json$/, "");
|
|
197
|
-
|
|
114
|
+
let parsed: {
|
|
115
|
+
instance_id?: string;
|
|
116
|
+
last_heartbeat?: string;
|
|
117
|
+
platform?: unknown;
|
|
118
|
+
session_id?: string;
|
|
119
|
+
} | null = null;
|
|
198
120
|
try {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
let parsed: {
|
|
204
|
-
instance_id?: string;
|
|
205
|
-
last_heartbeat?: string;
|
|
206
|
-
platform?: unknown;
|
|
207
|
-
session_id?: string;
|
|
208
|
-
v3_generation_id?: unknown;
|
|
209
|
-
} | null = null;
|
|
210
|
-
try {
|
|
211
|
-
const value = JSON.parse(revision.bytes.toString("utf8"));
|
|
212
|
-
if (value && typeof value === "object" && !Array.isArray(value)) parsed = value;
|
|
213
|
-
} catch {
|
|
214
|
-
parsed = null;
|
|
215
|
-
}
|
|
121
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
122
|
+
} catch {
|
|
123
|
+
parsed = null;
|
|
124
|
+
}
|
|
216
125
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
null,
|
|
227
|
-
"unparseable",
|
|
228
|
-
nowSec - mtimeSecs,
|
|
229
|
-
),
|
|
230
|
-
);
|
|
231
|
-
recordHeartbeatCleanupResult(coordRoot, path, idFromFile, result, uncertainOwners);
|
|
232
|
-
if (result.state === "deleted") {
|
|
233
|
-
heartbeatsRemoved.push(f);
|
|
234
|
-
}
|
|
126
|
+
if (parsed === null) {
|
|
127
|
+
// Unparseable: only reap if the file itself is mtime-old.
|
|
128
|
+
if (mtimeSecs(path) < cutoff) {
|
|
129
|
+
try {
|
|
130
|
+
const ageSecs = nowSec - mtimeSecs(path);
|
|
131
|
+
if (
|
|
132
|
+
!emitSwept(coordRoot, idFromFile, "claude-code", idFromFile, "unparseable", ageSecs)
|
|
133
|
+
) {
|
|
134
|
+
continue;
|
|
235
135
|
}
|
|
236
|
-
|
|
136
|
+
unlinkSync(path);
|
|
137
|
+
heartbeatsRemoved.push(f);
|
|
138
|
+
} catch {
|
|
139
|
+
/* swallow */
|
|
237
140
|
}
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
238
144
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const generationId =
|
|
246
|
-
typeof parsed.v3_generation_id === "string" &&
|
|
247
|
-
/^gen_[A-Za-z0-9._-]+$/.test(parsed.v3_generation_id)
|
|
248
|
-
? (parsed.v3_generation_id as `gen_${string}`)
|
|
249
|
-
: null;
|
|
250
|
-
const ts = parsed.last_heartbeat
|
|
251
|
-
? Math.floor(Date.parse(parsed.last_heartbeat) / 1000)
|
|
252
|
-
: Number.NaN;
|
|
253
|
-
|
|
254
|
-
if (!parsed.last_heartbeat || !Number.isFinite(ts)) {
|
|
255
|
-
// No / NaN last_heartbeat: can't trust content; gate on mtime.
|
|
256
|
-
if (mtimeSecs < cutoff) {
|
|
257
|
-
const result = deleteFileIfUnchanged(path, revision, () =>
|
|
258
|
-
emitSwept(
|
|
259
|
-
coordRoot,
|
|
260
|
-
instanceId,
|
|
261
|
-
adapter,
|
|
262
|
-
sessionId,
|
|
263
|
-
generationId,
|
|
264
|
-
"missing_ts",
|
|
265
|
-
nowSec - mtimeSecs,
|
|
266
|
-
),
|
|
267
|
-
);
|
|
268
|
-
recordHeartbeatCleanupResult(coordRoot, path, idFromFile, result, uncertainOwners);
|
|
269
|
-
if (result.state === "deleted") {
|
|
270
|
-
heartbeatsRemoved.push(f);
|
|
271
|
-
}
|
|
272
|
-
} else if (parsed.instance_id) {
|
|
273
|
-
liveInstanceIds.add(parsed.instance_id);
|
|
274
|
-
}
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
145
|
+
const instanceId = parsed.instance_id ?? idFromFile;
|
|
146
|
+
const adapter = eventAdapterIdV3FromPlatform(parsed.platform, { context: "stale-sweep" });
|
|
147
|
+
const sessionId = parsed.session_id ?? instanceId;
|
|
148
|
+
const ts = parsed.last_heartbeat
|
|
149
|
+
? Math.floor(Date.parse(parsed.last_heartbeat) / 1000)
|
|
150
|
+
: Number.NaN;
|
|
277
151
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (result.state === "deleted") {
|
|
293
|
-
heartbeatsRemoved.push(f);
|
|
294
|
-
}
|
|
295
|
-
return;
|
|
152
|
+
if (!parsed.last_heartbeat || !Number.isFinite(ts)) {
|
|
153
|
+
// No / NaN last_heartbeat: can't trust content; gate on mtime.
|
|
154
|
+
if (mtimeSecs(path) < cutoff) {
|
|
155
|
+
if (
|
|
156
|
+
!emitSwept(
|
|
157
|
+
coordRoot,
|
|
158
|
+
instanceId,
|
|
159
|
+
adapter,
|
|
160
|
+
sessionId,
|
|
161
|
+
"missing_ts",
|
|
162
|
+
nowSec - mtimeSecs(path),
|
|
163
|
+
)
|
|
164
|
+
) {
|
|
165
|
+
continue;
|
|
296
166
|
}
|
|
167
|
+
unlinkSync(path);
|
|
168
|
+
heartbeatsRemoved.push(f);
|
|
169
|
+
} else if (parsed.instance_id) {
|
|
170
|
+
liveInstanceIds.add(parsed.instance_id);
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
297
174
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
175
|
+
if (ts < cutoff) {
|
|
176
|
+
// Legitimate stale prune (valid timestamp, past the freshness cutoff).
|
|
177
|
+
if (!emitSwept(coordRoot, instanceId, adapter, sessionId, "stale", nowSec - ts)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
unlinkSync(path);
|
|
181
|
+
heartbeatsRemoved.push(f);
|
|
182
|
+
continue;
|
|
303
183
|
}
|
|
184
|
+
|
|
185
|
+
if (parsed.instance_id) liveInstanceIds.add(parsed.instance_id);
|
|
304
186
|
}
|
|
305
187
|
}
|
|
306
188
|
|
|
307
189
|
// 2. Prune pid-map entries whose instance has no live heartbeat.
|
|
308
|
-
const pidmapDir =
|
|
309
|
-
for (const recovery of existsSync(pidmapDir)
|
|
310
|
-
? recoverInterruptedFileDeletions(
|
|
311
|
-
pidmapDir,
|
|
312
|
-
(source) => dirname(source) === pidmapDir && /^\d+$/.test(basename(source)),
|
|
313
|
-
)
|
|
314
|
-
: [])
|
|
315
|
-
recordCleanupFailure(coordRoot, recovery.sourcePath ?? pidmapDir, recovery);
|
|
190
|
+
const pidmapDir = join(coordRoot, ".harnery", "pid-map");
|
|
316
191
|
if (existsSync(pidmapDir)) {
|
|
317
192
|
for (const f of readdirSync(pidmapDir)) {
|
|
318
|
-
if (f.startsWith(".harnery-delete-")) continue;
|
|
319
193
|
const path = join(pidmapDir, f);
|
|
320
194
|
try {
|
|
321
|
-
const
|
|
322
|
-
if (!revision) continue;
|
|
323
|
-
const row = revision.bytes.toString("utf8").trim();
|
|
195
|
+
const row = readFileSync(path, "utf8").trim();
|
|
324
196
|
const ownerId = row.split("\t")[0]?.trim() ?? "";
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
(!liveInstanceIds.has(ownerId) &&
|
|
329
|
-
!retainsHeartbeat(coordRoot, d, ownerId, uncertainOwners))
|
|
330
|
-
) {
|
|
331
|
-
const result = deleteFileIfUnchanged(
|
|
332
|
-
path,
|
|
333
|
-
revision,
|
|
334
|
-
() => !ownerId || !retainsHeartbeat(coordRoot, d, ownerId, uncertainOwners),
|
|
335
|
-
);
|
|
336
|
-
recordCleanupFailure(coordRoot, path, result);
|
|
337
|
-
if (result.state === "deleted") pidmapsRemoved += 1;
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
if (/^[A-Za-z0-9_-]{1,128}$/.test(ownerId)) {
|
|
341
|
-
withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(ownerId)], prune);
|
|
342
|
-
} else if (!ownerId) {
|
|
343
|
-
prune();
|
|
197
|
+
if (!ownerId || !liveInstanceIds.has(ownerId)) {
|
|
198
|
+
unlinkSync(path);
|
|
199
|
+
pidmapsRemoved += 1;
|
|
344
200
|
}
|
|
345
201
|
} catch {
|
|
346
202
|
/* swallow */
|
|
@@ -350,16 +206,7 @@ export function staleSweep(coordRoot: string): {
|
|
|
350
206
|
|
|
351
207
|
// 3. Prune prompt hashes for dead owners. Resource hashes retain their own
|
|
352
208
|
// count so existing peer-hash telemetry keeps its original meaning.
|
|
353
|
-
const agentsDir =
|
|
354
|
-
for (const recovery of existsSync(agentsDir)
|
|
355
|
-
? recoverInterruptedFileDeletions(
|
|
356
|
-
agentsDir,
|
|
357
|
-
(source) =>
|
|
358
|
-
dirname(source) === agentsDir &&
|
|
359
|
-
/^\.last-(?:resource|peer)-hash\.[A-Za-z0-9_-]{1,128}$/.test(basename(source)),
|
|
360
|
-
)
|
|
361
|
-
: [])
|
|
362
|
-
recordCleanupFailure(coordRoot, recovery.sourcePath ?? agentsDir, recovery);
|
|
209
|
+
const agentsDir = join(coordRoot, ".harnery");
|
|
363
210
|
if (existsSync(agentsDir)) {
|
|
364
211
|
for (const f of readdirSync(agentsDir)) {
|
|
365
212
|
const resourceHash = f.startsWith(".last-resource-hash.");
|
|
@@ -370,25 +217,13 @@ export function staleSweep(coordRoot: string): {
|
|
|
370
217
|
// Ignore atomic-write temporary files and preserve hashes when a
|
|
371
218
|
// retained heartbeat's liveness could not be determined safely.
|
|
372
219
|
if (!/^[A-Za-z0-9_-]{1,128}$/.test(owner)) continue;
|
|
220
|
+
if (existsSync(join(d, `${owner}.json`))) continue;
|
|
373
221
|
}
|
|
374
|
-
if (!liveInstanceIds.has(owner)
|
|
222
|
+
if (!liveInstanceIds.has(owner)) {
|
|
375
223
|
try {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const revision = captureFileRevision(path);
|
|
380
|
-
if (!revision) return;
|
|
381
|
-
const result = deleteFileIfUnchanged(
|
|
382
|
-
path,
|
|
383
|
-
revision,
|
|
384
|
-
() => !retainsHeartbeat(coordRoot, d, owner, uncertainOwners),
|
|
385
|
-
);
|
|
386
|
-
recordCleanupFailure(coordRoot, path, result);
|
|
387
|
-
if (result.state === "deleted") {
|
|
388
|
-
if (resourceHash) resourceHashesRemoved += 1;
|
|
389
|
-
else peerHashesRemoved += 1;
|
|
390
|
-
}
|
|
391
|
-
});
|
|
224
|
+
unlinkSync(join(agentsDir, f));
|
|
225
|
+
if (resourceHash) resourceHashesRemoved += 1;
|
|
226
|
+
else peerHashesRemoved += 1;
|
|
392
227
|
} catch {
|
|
393
228
|
/* swallow */
|
|
394
229
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { lstatSync, readdirSync } from "node:fs";
|
|
2
|
+
import { lstatSync, readdirSync, type Stats } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { ARTIFACT_MANIFEST } from "./constants.ts";
|
|
5
5
|
|
|
@@ -36,11 +36,14 @@ export function artifactRootEntries(path: string): string {
|
|
|
36
36
|
* Keep the prior root activity while its payload entries match the checkpoint.
|
|
37
37
|
* A root deletion or rename changes that fingerprint; nested changes retain
|
|
38
38
|
* their ordinary filesystem timestamps. Symlinks are inspected, never followed.
|
|
39
|
+
* `visit` sees every entry's lstat exactly once, including a root manifest the
|
|
40
|
+
* activity rules skip, so a caller can size the tree in the same walk.
|
|
39
41
|
*/
|
|
40
42
|
export function readArtifactActivity(
|
|
41
43
|
path: string,
|
|
42
44
|
now: Date,
|
|
43
45
|
previous?: ArtifactActivity,
|
|
46
|
+
visit?: (entry: string, stat: Stats) => void,
|
|
44
47
|
): ArtifactActivity {
|
|
45
48
|
const entries = artifactRootEntries(path);
|
|
46
49
|
const nowMs = now.getTime();
|
|
@@ -50,6 +53,7 @@ export function readArtifactActivity(
|
|
|
50
53
|
: 0;
|
|
51
54
|
const walk = (entry: string, root = false): number => {
|
|
52
55
|
const stat = lstatSync(entry);
|
|
56
|
+
visit?.(entry, stat);
|
|
53
57
|
let latest =
|
|
54
58
|
root && previous?.root_entries_sha256 === entries
|
|
55
59
|
? accept(Date.parse(previous.last_changed_at))
|
|
@@ -58,7 +62,10 @@ export function readArtifactActivity(
|
|
|
58
62
|
for (const name of readdirSync(entry)) {
|
|
59
63
|
// Only this unit's own control manifest is metadata. Nested artifact
|
|
60
64
|
// manifests are payload from the containing workspace's point of view.
|
|
61
|
-
if (root && previous && name === ARTIFACT_MANIFEST)
|
|
65
|
+
if (root && previous && name === ARTIFACT_MANIFEST) {
|
|
66
|
+
if (visit) visit(join(entry, name), lstatSync(join(entry, name)));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
62
69
|
latest = Math.max(latest, walk(join(entry, name)));
|
|
63
70
|
}
|
|
64
71
|
}
|
|
@@ -14,7 +14,6 @@ import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
14
14
|
import { encodeLinkSafeComponent } from "../../lib/local-file-url.ts";
|
|
15
15
|
import { findLiveTunnelForOrigin, tunnelServesPaths } from "../../lib/tunnel/state.ts";
|
|
16
16
|
import { resolveWebPort } from "../config.ts";
|
|
17
|
-
import { createTunnelReadinessVerifier } from "../servers/index.ts";
|
|
18
17
|
import { stateFileMode } from "../storage/modes.ts";
|
|
19
18
|
import { ARTIFACT_MANIFEST } from "./constants.ts";
|
|
20
19
|
import { resolveArtifactRef, showArtifact } from "./index.ts";
|
|
@@ -331,12 +330,7 @@ function deliveryLinkBase(repoRoot: string, environment: DisplayEnvironment): st
|
|
|
331
330
|
// route: its gate would refuse every link on the card.
|
|
332
331
|
const tunnel =
|
|
333
332
|
environment.tunnelUrl === undefined
|
|
334
|
-
? findLiveTunnelForOrigin(
|
|
335
|
-
webPort,
|
|
336
|
-
`localhost:${webPort}`,
|
|
337
|
-
repoRoot,
|
|
338
|
-
createTunnelReadinessVerifier({ coordRoot: repoRoot }),
|
|
339
|
-
)
|
|
333
|
+
? findLiveTunnelForOrigin(webPort, `localhost:${webPort}`, repoRoot)
|
|
340
334
|
: null;
|
|
341
335
|
const tunnelUrl =
|
|
342
336
|
environment.tunnelUrl === undefined
|