@timurproko/a1 0.1.8-dev.560 → 0.1.8-dev.563
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/bin/guardian.js +5 -0
- package/dist/app/session-shell/session-shell-root.js +7 -5
- package/dist/app/session-shell/session-shell.js +7 -12
- package/dist/foundation/launch-guardian/main.js +1 -2
- package/dist/foundation/lifecycle/model.d.ts +1 -1
- package/dist/foundation/release/bootstrap.js +72 -29
- package/dist/foundation/release/cohort-state.d.ts +2 -0
- package/dist/foundation/release/cohort-state.js +35 -0
- package/dist/foundation/release/index.d.ts +1 -0
- package/dist/foundation/release/index.js +1 -0
- package/dist/foundation/release/ordinary-launch-selection.d.ts +14 -0
- package/dist/foundation/release/ordinary-launch-selection.js +15 -0
- package/dist/foundation/supervision/server.js +15 -4
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/package.json +1 -1
package/bin/guardian.js
CHANGED
|
@@ -26,6 +26,11 @@ Promise.resolve().then(() => runLaunchGuardian({
|
|
|
26
26
|
})).then(
|
|
27
27
|
code => { process.exitCode = code; },
|
|
28
28
|
error => {
|
|
29
|
+
if (error instanceof Error && "code" in error && error.code === "release-superseded" && typeof process.send === "function") {
|
|
30
|
+
process.send({ type: "a1-release-reselection" });
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
29
34
|
console.error(error instanceof Error ? error.message : String(error));
|
|
30
35
|
process.exitCode = 1;
|
|
31
36
|
},
|
|
@@ -687,11 +687,13 @@ export class OwnedUiSessionShellRoot {
|
|
|
687
687
|
.flatMap(diagnostic => renderPiShellPackageUpdateNotice(diagnostic.message.split("\n").filter(line => line.startsWith("- ")).map(line => line.slice(2)), width)));
|
|
688
688
|
rows.push(...diagnostics
|
|
689
689
|
.filter(diagnostic => diagnostic.code === "changelog-collapsed" || diagnostic.code === "changelog-expanded")
|
|
690
|
-
// Rationale:
|
|
691
|
-
//
|
|
692
|
-
.flatMap(diagnostic =>
|
|
693
|
-
?
|
|
694
|
-
:
|
|
690
|
+
// Rationale: bare A1 translates either startup diagnostic into transient dock chrome;
|
|
691
|
+
// the pinned layout retains Pi's collapsed hint or expanded document.
|
|
692
|
+
.flatMap(diagnostic => this.#customViewport
|
|
693
|
+
? []
|
|
694
|
+
: diagnostic.code === "changelog-collapsed"
|
|
695
|
+
? createPiShellCollapsedChangelog().render(width)
|
|
696
|
+
: createPiShellChangelog(diagnostic.message).render(width)));
|
|
695
697
|
rows.push(...diagnostics
|
|
696
698
|
.filter(diagnostic => diagnostic.code !== "engine-startup" && diagnostic.code !== "package-updates"
|
|
697
699
|
&& diagnostic.code !== "changelog-collapsed" && diagnostic.code !== "changelog-expanded")
|
|
@@ -16,7 +16,6 @@ import { SKILLS_COMMAND_NAME, findSkillByArgument, rewriteSkillsTunnelSubmission
|
|
|
16
16
|
import { createPiQueuedInputStatus, createPiShellFooter, createPiShellHeader, createPiShellLoadedResources, createPiShellStatus, } from "../../integrations/pi/components/shell-footer-status.js";
|
|
17
17
|
import { createPiShellArmin, createPiShellAuthProviderSelector, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellModelSelector, createPiShellModelsDialog, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellSkillsSelector, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
|
|
18
18
|
import { createPiShellChangelog, createPiShellHotkeys, createPiShellSessionInfo, renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
|
|
19
|
-
import { CHANGELOG_ROUTE } from "../../features/owned-ui/reference-routes.js";
|
|
20
19
|
import { createPiShellTranscriptComponent, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellTranscriptBlock, } from "../../integrations/pi/components/shell-presenters-transcript.js";
|
|
21
20
|
import { onPiThemeChange, piTheme } from "../../integrations/pi/components/upstream/theme/theme.js";
|
|
22
21
|
import { DamageAwareTerminalAdapter } from "../../integrations/pi/tui-runtime/damage-aware-terminal.js";
|
|
@@ -56,7 +55,7 @@ export class OwnedUiSessionShell {
|
|
|
56
55
|
#shutdownPromise;
|
|
57
56
|
#disposePromise;
|
|
58
57
|
#pointerReporting = false;
|
|
59
|
-
// Invariant: the startup release
|
|
58
|
+
// Invariant: the startup release-note diagnostic creates at most one dock notice per shell.
|
|
60
59
|
#startupChangelogHandled = false;
|
|
61
60
|
#customViewport;
|
|
62
61
|
#responseCopy;
|
|
@@ -1689,22 +1688,17 @@ export class OwnedUiSessionShell {
|
|
|
1689
1688
|
listener(view);
|
|
1690
1689
|
return view;
|
|
1691
1690
|
}
|
|
1692
|
-
// Rationale:
|
|
1693
|
-
//
|
|
1691
|
+
// Rationale: release details stay one explicit command away; startup needs only the same quiet,
|
|
1692
|
+
// non-transcript acknowledgement that bare A1 uses for model and thinking changes.
|
|
1694
1693
|
#presentStartupChangelog(view) {
|
|
1695
|
-
if (this.#startupChangelogHandled || !this.#customViewport
|
|
1694
|
+
if (this.#startupChangelogHandled || !this.#customViewport)
|
|
1696
1695
|
return;
|
|
1697
|
-
const diagnostic = view.diagnostics.find(candidate => candidate.code === "changelog-expanded");
|
|
1696
|
+
const diagnostic = view.diagnostics.find(candidate => candidate.code === "changelog-collapsed" || candidate.code === "changelog-expanded");
|
|
1698
1697
|
// Invariant: a view synchronized before the runtime starts is not the moment of arrival; start() replays it.
|
|
1699
1698
|
if (diagnostic === undefined || !this.runtime.active)
|
|
1700
1699
|
return;
|
|
1701
1700
|
this.#startupChangelogHandled = true;
|
|
1702
|
-
|
|
1703
|
-
if (this.#dialogHandle !== undefined || !this.root.usesDefaultInputSurface())
|
|
1704
|
-
return;
|
|
1705
|
-
if (!this.#routeHost.claims(CHANGELOG_ROUTE))
|
|
1706
|
-
return;
|
|
1707
|
-
this.#openOwnedRoute(CHANGELOG_ROUTE, { document: diagnostic.message });
|
|
1701
|
+
this.root.appendWorkflowStatus(STARTUP_CHANGELOG_NOTICE);
|
|
1708
1702
|
}
|
|
1709
1703
|
#openOwnedRoute(route, input) {
|
|
1710
1704
|
const surface = this.#routeHost?.open(route, input) ?? null;
|
|
@@ -2117,6 +2111,7 @@ function workflowAdapterResult(result) {
|
|
|
2117
2111
|
const INTERRUPT = "\u0003";
|
|
2118
2112
|
const INTERRUPT_CHORD_MS = 1_500;
|
|
2119
2113
|
const RELOAD_SURFACE_MIN_VISIBLE_MS = 400;
|
|
2114
|
+
const STARTUP_CHANGELOG_NOTICE = "Run /changelog to view the full release notes.";
|
|
2120
2115
|
// Rationale: the outro is not configurable; the switch only decides whether this plan plays.
|
|
2121
2116
|
const QUIT_OUTRO_EFFECT = "fall";
|
|
2122
2117
|
const QUIT_OUTRO_DURATION_MS = 800;
|
|
@@ -2,10 +2,9 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { PRODUCT_TEXT } from "../../product-identity.js";
|
|
3
3
|
import { readLaunchContext } from "../launch-context/index.js";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
|
-
import { assertLaunchProfileId, sessionSelectionArguments } from "../lifecycle/index.js";
|
|
5
|
+
import { assertLaunchProfileId, resolveCohortEndpoint, resolveProductPaths, sessionSelectionArguments } from "../lifecycle/index.js";
|
|
6
6
|
import { closeVerifiedContainment, DarwinNativeProcessInspector, LinuxNativeProcessInspector, NativeGuardianContainment, resolveProcessGuardianPath, verifyProcessGuardianArtifact, WindowsNativeProcessInspector, } from "../process-containment/index.js";
|
|
7
7
|
import { SupervisorClient } from "../protocol/index.js";
|
|
8
|
-
import { resolveCohortEndpoint, resolveProductPaths } from "../supervision/index.js";
|
|
9
8
|
import { markStartupPhase } from "../startup/index.js";
|
|
10
9
|
export async function runLaunchGuardian(options) {
|
|
11
10
|
assertLaunchProfileId(options.profileId);
|
|
@@ -54,7 +54,7 @@ export interface CommandResult {
|
|
|
54
54
|
readonly ok: boolean;
|
|
55
55
|
readonly revision: number;
|
|
56
56
|
readonly error?: {
|
|
57
|
-
readonly code: "invalid-command" | "not-found" | "stale-generation" | "capability-error" | "containment-unsupported" | "ownership-error" | "driver-error";
|
|
57
|
+
readonly code: "invalid-command" | "not-found" | "stale-generation" | "capability-error" | "containment-unsupported" | "ownership-error" | "release-superseded" | "driver-error";
|
|
58
58
|
readonly message: string;
|
|
59
59
|
};
|
|
60
60
|
}
|
|
@@ -5,6 +5,7 @@ import { connect } from "node:net";
|
|
|
5
5
|
import { platform } from "node:os";
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
7
|
import { selectCohortLaunch } from "./cohort-selection.js";
|
|
8
|
+
import { selectOrdinaryLaunchReleaseId } from "./ordinary-launch-selection.js";
|
|
8
9
|
import { CohortStateStore } from "./cohort-state.js";
|
|
9
10
|
import { assertLaunchProfileId, createSupervisorStartupAttempt, readSupervisorStartupResult, resolveCohortEndpoint, resolveProductPaths, sessionSelectionArguments } from "../lifecycle/index.js";
|
|
10
11
|
import { encodeFrame, LineFrameDecoder } from "../protocol/index.js";
|
|
@@ -18,7 +19,11 @@ import { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDoc
|
|
|
18
19
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
19
20
|
import { markStartupPhase } from "../startup/index.js";
|
|
20
21
|
import { launchContractTarget, readLaunchContext, withLaunchContext } from "../launch-context/index.js";
|
|
22
|
+
const RELEASE_RESELECTION_MESSAGE = "a1-release-reselection";
|
|
21
23
|
export async function runBootstrap(options) {
|
|
24
|
+
return await runBootstrapAttempt(options, 1);
|
|
25
|
+
}
|
|
26
|
+
async function runBootstrapAttempt(options, retriesRemaining) {
|
|
22
27
|
const environment = withLaunchContext(options.environment ?? process.env, { launchProfile: options.launchIntent?.profileId ?? "a1" });
|
|
23
28
|
await markStartupPhase(environment, "bootstrap-start");
|
|
24
29
|
const launchProfileId = options.launchIntent?.profileId ?? "a1";
|
|
@@ -29,6 +34,15 @@ export async function runBootstrap(options) {
|
|
|
29
34
|
const output = options.output ?? process.stderr;
|
|
30
35
|
const paths = resolveProductPaths(environment);
|
|
31
36
|
await mkdir(paths.runtimeDir, { recursive: true, mode: 0o700 });
|
|
37
|
+
const launchSelected = async (release) => {
|
|
38
|
+
const result = await launchUi(release, environment, sessionArgs);
|
|
39
|
+
if (!result.releaseSuperseded)
|
|
40
|
+
return result.code;
|
|
41
|
+
if (retriesRemaining > 0)
|
|
42
|
+
return await runBootstrapAttempt(options, retriesRemaining - 1);
|
|
43
|
+
output.write(`${PRODUCT_TEXT.diagnostic("active release changed repeatedly during startup; launch could not converge")}\n`);
|
|
44
|
+
return 1;
|
|
45
|
+
};
|
|
32
46
|
const stateStore = new CohortStateStore(paths.dataDir);
|
|
33
47
|
let state = await stateStore.read();
|
|
34
48
|
const launchDuringUpdate = async () => {
|
|
@@ -41,7 +55,7 @@ export async function runBootstrap(options) {
|
|
|
41
55
|
return null;
|
|
42
56
|
const retained = await readCertifiedReleaseManifest(prior, resolve(paths.dataDir, "releases"));
|
|
43
57
|
await ensureSupervisor(retained, environment);
|
|
44
|
-
return await
|
|
58
|
+
return await launchSelected(retained);
|
|
45
59
|
};
|
|
46
60
|
const duringUpdate = await launchDuringUpdate();
|
|
47
61
|
if (duringUpdate !== null)
|
|
@@ -82,7 +96,7 @@ export async function runBootstrap(options) {
|
|
|
82
96
|
&& endpoint.contentDigest === active.contentDigest;
|
|
83
97
|
if (endpointMatches && probe === "live-verified") {
|
|
84
98
|
const retained = await readCertifiedReleaseManifest(active, resolve(paths.dataDir, "releases"));
|
|
85
|
-
return await
|
|
99
|
+
return await launchSelected(retained);
|
|
86
100
|
}
|
|
87
101
|
if (endpoint === null || probe === "dead") {
|
|
88
102
|
if (endpoint)
|
|
@@ -100,7 +114,7 @@ export async function runBootstrap(options) {
|
|
|
100
114
|
await markStartupPhase(environment, "replacement-supervisor-start");
|
|
101
115
|
await ensureSupervisor(retained, environment);
|
|
102
116
|
await markStartupPhase(environment, "replacement-supervisor-ready");
|
|
103
|
-
return await
|
|
117
|
+
return await launchSelected(retained);
|
|
104
118
|
}
|
|
105
119
|
}
|
|
106
120
|
// Concurrency: an installation being replaced is not a reason to refuse a launch. While
|
|
@@ -112,7 +126,7 @@ export async function runBootstrap(options) {
|
|
|
112
126
|
candidate = await materializeRelease(options.packageRoot, paths.dataDir);
|
|
113
127
|
}
|
|
114
128
|
catch (error) {
|
|
115
|
-
const fallback = await launchRetainedActive(state, paths, environment,
|
|
129
|
+
const fallback = await launchRetainedActive(state, paths, environment, launchSelected);
|
|
116
130
|
if (fallback === null)
|
|
117
131
|
throw error;
|
|
118
132
|
return fallback;
|
|
@@ -121,24 +135,32 @@ export async function runBootstrap(options) {
|
|
|
121
135
|
const afterMaterialization = await launchDuringUpdate();
|
|
122
136
|
if (afterMaterialization !== null)
|
|
123
137
|
return afterMaterialization;
|
|
138
|
+
state = await stateStore.read();
|
|
139
|
+
const ordinarySelection = !activeIsLaunchable
|
|
140
|
+
? candidate.releaseId
|
|
141
|
+
: selectOrdinaryLaunchReleaseId(candidate, state.references.active === null ? undefined : state.releases[state.references.active]);
|
|
142
|
+
if (ordinarySelection !== candidate.releaseId) {
|
|
143
|
+
const retainedRecord = state.releases[ordinarySelection];
|
|
144
|
+
if (!retainedRecord || retainedRecord.approval !== "approved")
|
|
145
|
+
throw new Error("selected active release is not approved");
|
|
146
|
+
const retained = await readCertifiedReleaseManifest(retainedRecord, resolve(paths.dataDir, "releases"));
|
|
147
|
+
await ensureSupervisor(retained, environment);
|
|
148
|
+
return await launchSelected(retained);
|
|
149
|
+
}
|
|
124
150
|
await stateStore.recordCandidate(candidate);
|
|
125
151
|
state = await stateStore.read();
|
|
126
|
-
if (
|
|
152
|
+
if (state.releases[candidate.releaseId]?.approval !== "approved") {
|
|
127
153
|
const diagnosticsPath = await certifyMaterializedRelease(candidate, paths.dataDir);
|
|
128
154
|
await stateStore.approve(candidate.releaseId, diagnosticsPath);
|
|
129
|
-
await stateStore.activate(candidate.releaseId);
|
|
130
|
-
state = await stateStore.read();
|
|
131
155
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
await stateStore.approve(candidate.releaseId, diagnosticsPath);
|
|
141
|
-
state = await stateStore.read();
|
|
156
|
+
state = await stateStore.activateForLaunch(candidate.releaseId, !activeIsLaunchable ? activeId ?? undefined : undefined);
|
|
157
|
+
if (state.references.active !== candidate.releaseId) {
|
|
158
|
+
const retainedRecord = state.references.active === null ? undefined : state.releases[state.references.active];
|
|
159
|
+
if (!retainedRecord || retainedRecord.approval !== "approved")
|
|
160
|
+
throw new Error("ordinary launch has no approved active release");
|
|
161
|
+
const retained = await readCertifiedReleaseManifest(retainedRecord, resolve(paths.dataDir, "releases"));
|
|
162
|
+
await ensureSupervisor(retained, environment);
|
|
163
|
+
return await launchSelected(retained);
|
|
142
164
|
}
|
|
143
165
|
// Protocol: the candidate's own endpoint decides whether this launch attaches or starts a supervisor.
|
|
144
166
|
// A cohort other than this one is not in the way: it listens somewhere else.
|
|
@@ -146,7 +168,7 @@ export async function runBootstrap(options) {
|
|
|
146
168
|
endpoint = await readEndpointMetadata(endpointPaths.endpointMetadataPath);
|
|
147
169
|
if (endpoint === null) {
|
|
148
170
|
const legacy = await readEndpointMetadata(paths.endpointMetadataPath);
|
|
149
|
-
if (legacy
|
|
171
|
+
if (legacy?.releaseId === candidate.releaseId) {
|
|
150
172
|
endpoint = legacy;
|
|
151
173
|
endpointPaths = { endpoint: paths.endpoint, endpointMetadataPath: paths.endpointMetadataPath };
|
|
152
174
|
}
|
|
@@ -170,11 +192,14 @@ export async function runBootstrap(options) {
|
|
|
170
192
|
await removeEndpointArtifacts(endpointPaths.endpointMetadataPath, endpointPaths.endpoint);
|
|
171
193
|
}
|
|
172
194
|
if (decision.action === "launch-retained-ui") {
|
|
173
|
-
const
|
|
195
|
+
const retainedRecord = state.releases[decision.releaseId];
|
|
196
|
+
const retained = retainedRecord?.approval === "approved"
|
|
197
|
+
? await readCertifiedReleaseManifest(retainedRecord, resolve(paths.dataDir, "releases"))
|
|
198
|
+
: await verifyMaterializedRelease(decision.releaseRoot, undefined, resolve(paths.dataDir, "releases"));
|
|
174
199
|
if (decision.recordPending && endpoint) {
|
|
175
200
|
await stateStore.blockPending("candidate activation deferred by live non-resumable instances", endpoint.ownership.nonResumableInstanceIds);
|
|
176
201
|
}
|
|
177
|
-
const code = await
|
|
202
|
+
const code = await launchSelected(retained);
|
|
178
203
|
if (decision.recordPending)
|
|
179
204
|
await activatePendingAfterBlockerExit(candidate, stateStore, paths, environment);
|
|
180
205
|
return code;
|
|
@@ -204,15 +229,23 @@ export async function runBootstrap(options) {
|
|
|
204
229
|
const diagnosticsPath = await certifyMaterializedRelease(candidate, paths.dataDir);
|
|
205
230
|
await stateStore.approve(candidate.releaseId, diagnosticsPath);
|
|
206
231
|
}
|
|
207
|
-
|
|
208
|
-
await stateStore.
|
|
209
|
-
|
|
232
|
+
const activated = (await stateStore.read()).references.active === candidate.releaseId
|
|
233
|
+
? await stateStore.read()
|
|
234
|
+
: await stateStore.activateForLaunch(candidate.releaseId);
|
|
235
|
+
if (activated.references.active === candidate.releaseId)
|
|
236
|
+
selected = candidate;
|
|
237
|
+
else {
|
|
238
|
+
const retainedRecord = activated.references.active === null ? undefined : activated.releases[activated.references.active];
|
|
239
|
+
if (!retainedRecord || retainedRecord.approval !== "approved")
|
|
240
|
+
throw new Error("ordinary launch has no approved active release");
|
|
241
|
+
selected = await readCertifiedReleaseManifest(retainedRecord, resolve(paths.dataDir, "releases"));
|
|
242
|
+
}
|
|
210
243
|
}
|
|
211
244
|
else {
|
|
212
245
|
selected = await readMaterializedRelease(decision.releaseRoot);
|
|
213
246
|
}
|
|
214
247
|
await ensureSupervisor(selected, environment);
|
|
215
|
-
return await
|
|
248
|
+
return await launchSelected(selected);
|
|
216
249
|
}
|
|
217
250
|
/**
|
|
218
251
|
* Start the retained active cohort when the installed payload cannot be taken. The
|
|
@@ -220,7 +253,7 @@ export async function runBootstrap(options) {
|
|
|
220
253
|
* certifying anything new, and reports no selection when there is nothing safe
|
|
221
254
|
* to fall back to.
|
|
222
255
|
*/
|
|
223
|
-
async function launchRetainedActive(state, paths, environment,
|
|
256
|
+
async function launchRetainedActive(state, paths, environment, launch) {
|
|
224
257
|
const activeId = state.references.active;
|
|
225
258
|
const active = activeId === null ? undefined : state.releases[activeId];
|
|
226
259
|
if (!active || active.approval !== "approved")
|
|
@@ -228,8 +261,7 @@ async function launchRetainedActive(state, paths, environment, sessionArgs, outp
|
|
|
228
261
|
try {
|
|
229
262
|
const retained = await readCertifiedReleaseManifest(active, resolve(paths.dataDir, "releases"));
|
|
230
263
|
await ensureSupervisor(retained, environment);
|
|
231
|
-
|
|
232
|
-
return await launchUi(retained, environment, sessionArgs);
|
|
264
|
+
return await launch(retained);
|
|
233
265
|
}
|
|
234
266
|
catch {
|
|
235
267
|
return null;
|
|
@@ -300,13 +332,24 @@ async function launchUi(release, environment, sessionArgs) {
|
|
|
300
332
|
await markStartupPhase(environment, "bootstrap-selected");
|
|
301
333
|
const entry = await resolveReleaseEntryPoint(release, "bin/guardian.js");
|
|
302
334
|
return await new Promise((resolvePromise, rejectPromise) => {
|
|
335
|
+
let releaseSuperseded = false;
|
|
303
336
|
const child = spawn(process.execPath, [entry, ...sessionArgs], {
|
|
304
337
|
env: releaseEnvironment(environment, release, readLaunchContext(environment, "profile").launchProfile),
|
|
305
|
-
stdio: "inherit",
|
|
338
|
+
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
|
306
339
|
windowsHide: false,
|
|
307
340
|
});
|
|
341
|
+
child.on("message", message => {
|
|
342
|
+
if (message && typeof message === "object" && "type" in message && message.type === RELEASE_RESELECTION_MESSAGE) {
|
|
343
|
+
releaseSuperseded = true;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
308
346
|
child.once("error", rejectPromise);
|
|
309
|
-
child.once("close", (code, signal) => resolvePromise(
|
|
347
|
+
child.once("close", (code, signal) => resolvePromise({
|
|
348
|
+
code: releaseSuperseded
|
|
349
|
+
? code ?? 1
|
|
350
|
+
: restoreAfterOwnedExit(readLaunchContext(environment).launchProfile === "a1", code, signal),
|
|
351
|
+
releaseSuperseded,
|
|
352
|
+
}));
|
|
310
353
|
});
|
|
311
354
|
}
|
|
312
355
|
/**
|
|
@@ -140,6 +140,8 @@ export declare class CohortStateStore {
|
|
|
140
140
|
recordCandidate(release: MaterializedRelease): Promise<CohortState>;
|
|
141
141
|
approve(releaseId: string, diagnosticsPath: string): Promise<CohortState>;
|
|
142
142
|
activate(releaseId: string): Promise<CohortState>;
|
|
143
|
+
/** Atomically advances ordinary launch without allowing an older installation to roll active state back. */
|
|
144
|
+
activateForLaunch(releaseId: string, replaceUnlaunchableActiveId?: string): Promise<CohortState>;
|
|
143
145
|
blockPending(reason: string, blockerGenerationIds: readonly string[]): Promise<CohortState>;
|
|
144
146
|
rollback(ownershipReleased: boolean): Promise<CohortState>;
|
|
145
147
|
setRetention(releaseIds: readonly string[]): Promise<CohortState>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { selectOrdinaryLaunchReleaseId } from "./ordinary-launch-selection.js";
|
|
4
5
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
5
6
|
export const RELEASE_COHORT_SCHEMA = PRODUCT_IDENTITY.protocol.releaseCohortSchema;
|
|
6
7
|
const RELEASE_ID_PATTERN = /^[0-9A-Za-z.+_-]+-[a-f0-9]{20}$/;
|
|
@@ -135,6 +136,40 @@ export class CohortStateStore {
|
|
|
135
136
|
};
|
|
136
137
|
});
|
|
137
138
|
}
|
|
139
|
+
/** Atomically advances ordinary launch without allowing an older installation to roll active state back. */
|
|
140
|
+
async activateForLaunch(releaseId, replaceUnlaunchableActiveId) {
|
|
141
|
+
return await this.update(current => {
|
|
142
|
+
const release = requiredRelease(current, releaseId);
|
|
143
|
+
if (release.approval !== "approved")
|
|
144
|
+
throw new Error(`cannot activate unverified release ${releaseId}`);
|
|
145
|
+
const selectedId = current.references.active === replaceUnlaunchableActiveId
|
|
146
|
+
? releaseId
|
|
147
|
+
: selectOrdinaryLaunchReleaseId(release, current.references.active === null ? undefined : current.releases[current.references.active]);
|
|
148
|
+
if (selectedId !== releaseId) {
|
|
149
|
+
return {
|
|
150
|
+
...current,
|
|
151
|
+
references: {
|
|
152
|
+
...current.references,
|
|
153
|
+
pending: current.references.pending === releaseId ? null : current.references.pending,
|
|
154
|
+
approved: selectedId,
|
|
155
|
+
},
|
|
156
|
+
activation: activation("idle", null),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const prior = current.references.active;
|
|
160
|
+
return {
|
|
161
|
+
...current,
|
|
162
|
+
references: {
|
|
163
|
+
...current.references,
|
|
164
|
+
active: releaseId,
|
|
165
|
+
pending: null,
|
|
166
|
+
approved: releaseId,
|
|
167
|
+
rollback: prior && prior !== releaseId ? prior : current.references.rollback,
|
|
168
|
+
},
|
|
169
|
+
activation: activation("idle", null),
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
}
|
|
138
173
|
async blockPending(reason, blockerGenerationIds) {
|
|
139
174
|
return await this.update(current => ({ ...current, activation: { ...activation("blocked", reason), blockerGenerationIds: [...blockerGenerationIds] } }));
|
|
140
175
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { certifyMaterializedRelease, ensureSupervisor, recordParentCertifiedRelease, releaseEnvironment, releaseVerifiedIdleOwner, runBootstrap, startSupervisor, waitForProcessExit, waitForVerifiedEndpoint, } from "./bootstrap.js";
|
|
2
2
|
export type { BootstrapOptions, SupervisorStartupAttempt } from "./bootstrap.js";
|
|
3
3
|
export { selectCohortLaunch } from "./cohort-selection.js";
|
|
4
|
+
export { selectOrdinaryLaunchReleaseId } from "./ordinary-launch-selection.js";
|
|
4
5
|
export type { CohortLaunchDecision, OwnershipProbe } from "./cohort-selection.js";
|
|
5
6
|
export { CohortStateStore, RELEASE_COHORT_SCHEMA, emptyState, planProtectedReleases } from "./cohort-state.js";
|
|
6
7
|
export type { ActiveReleaseTransactionReference, CohortState, EndpointOwnership, ExternalReleaseHold, ExternalReleaseHoldAuthority, OrphanReleaseDisposition, ProtectedReleaseInputs, ProtectedReleasePlan, ReleaseApproval, ReleaseCleanupDiagnostic, ReleaseCleanupDisposition, ReleaseCleanupStage, ReleaseCleanupState, ReleaseCleanupWorkerRun, ReleaseCleanupWorkerStatus, ReleaseCleanupWorkerSummary, ReleaseRecord, ReleaseReferences, ReleaseRetentionReconciliation, SupervisorEndpointMetadata, } from "./cohort-state.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { certifyMaterializedRelease, ensureSupervisor, recordParentCertifiedRelease, releaseEnvironment, releaseVerifiedIdleOwner, runBootstrap, startSupervisor, waitForProcessExit, waitForVerifiedEndpoint, } from "./bootstrap.js";
|
|
2
2
|
export { selectCohortLaunch } from "./cohort-selection.js";
|
|
3
|
+
export { selectOrdinaryLaunchReleaseId } from "./ordinary-launch-selection.js";
|
|
3
4
|
export { CohortStateStore, RELEASE_COHORT_SCHEMA, emptyState, planProtectedReleases } from "./cohort-state.js";
|
|
4
5
|
export { DEPENDENCY_LAYER_MANIFEST, DEPENDENCY_LAYER_SCHEMA, RUNTIME_PAYLOAD_INVENTORY, dependencyLayerCertificationPath, dependencyReference, generateDependencyRuntimePayload, legacyDependencyLayerCertificationPath, materializeDependencyLayer, readCertifiedDependencyLayer, selectDependencyRuntimePayload, selectPublishedDependencyRuntimePayload, verifyDependencyLayer, } from "./dependency-layer.js";
|
|
5
6
|
export { listRecordedEndpoints, liveReleaseIds, probeOwnership, readEndpointMetadata, removeEndpointArtifacts, sweepDeadEndpoints } from "./endpoints.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface OrdinaryLaunchRelease {
|
|
2
|
+
readonly releaseId: string;
|
|
3
|
+
readonly packageVersion: string;
|
|
4
|
+
}
|
|
5
|
+
interface ApprovedOrdinaryLaunchRelease extends OrdinaryLaunchRelease {
|
|
6
|
+
readonly approval: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Ordinary launch has no rollback authority: it may advance to its installed package, but an
|
|
10
|
+
* older package installation must follow a newer approved active release instead of moving the
|
|
11
|
+
* shared selector backward. Explicit update/rollback uses CohortStateStore.activate directly.
|
|
12
|
+
*/
|
|
13
|
+
export declare function selectOrdinaryLaunchReleaseId(candidate: OrdinaryLaunchRelease, active?: ApprovedOrdinaryLaunchRelease): string;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { compare, valid as validSemver } from "semver";
|
|
2
|
+
/**
|
|
3
|
+
* Ordinary launch has no rollback authority: it may advance to its installed package, but an
|
|
4
|
+
* older package installation must follow a newer approved active release instead of moving the
|
|
5
|
+
* shared selector backward. Explicit update/rollback uses CohortStateStore.activate directly.
|
|
6
|
+
*/
|
|
7
|
+
export function selectOrdinaryLaunchReleaseId(candidate, active) {
|
|
8
|
+
if (!active || active.approval !== "approved")
|
|
9
|
+
return candidate.releaseId;
|
|
10
|
+
const candidateVersion = validSemver(candidate.packageVersion);
|
|
11
|
+
const activeVersion = validSemver(active.packageVersion);
|
|
12
|
+
if (!candidateVersion || !activeVersion)
|
|
13
|
+
throw new Error("ordinary launch release versions must be valid semantic versions");
|
|
14
|
+
return compare(activeVersion, candidateVersion) >= 0 ? active.releaseId : candidate.releaseId;
|
|
15
|
+
}
|
|
@@ -271,7 +271,7 @@ export class SupervisorServer {
|
|
|
271
271
|
let result;
|
|
272
272
|
try {
|
|
273
273
|
if (command.type === "create-launch-instance")
|
|
274
|
-
this.#createLaunchInstance(command, socket, clientId);
|
|
274
|
+
await this.#createLaunchInstance(command, socket, clientId);
|
|
275
275
|
if (command.type === "activate-launch-instance")
|
|
276
276
|
this.#activateLaunchInstance(command, socket, clientId);
|
|
277
277
|
if (command.type === "begin-launch-instance-stop")
|
|
@@ -289,17 +289,28 @@ export class SupervisorServer {
|
|
|
289
289
|
}
|
|
290
290
|
catch (error) {
|
|
291
291
|
const message = error instanceof Error ? error.message : String(error);
|
|
292
|
-
const
|
|
292
|
+
const explicitCode = error instanceof Error && "code" in error ? error.code : undefined;
|
|
293
|
+
const code = explicitCode === "release-superseded"
|
|
294
|
+
? "release-superseded"
|
|
295
|
+
: /not found/i.test(message) ? "not-found"
|
|
296
|
+
: /owner|ownership|authenticated/i.test(message) ? "ownership-error" : "driver-error";
|
|
293
297
|
result = { requestId: command.requestId, ok: false, revision: this.#revision, error: { code, message } };
|
|
294
298
|
}
|
|
295
299
|
this.#results.set(resultKey, result);
|
|
296
300
|
this.#send(socket, { type: "command-result", result });
|
|
297
301
|
}
|
|
298
|
-
#createLaunchInstance(command, socket, clientId) {
|
|
302
|
+
async #createLaunchInstance(command, socket, clientId) {
|
|
299
303
|
assertNativeProcessIdentity(command.guardianIdentity);
|
|
300
304
|
const existing = this.store.loadLaunchInstance(command.instanceId);
|
|
305
|
+
if (!existing && this.readActiveReleaseId) {
|
|
306
|
+
const active = await this.readActiveReleaseId().catch(() => null);
|
|
307
|
+
if (active !== null)
|
|
308
|
+
this.#superseded = active !== this.release.releaseId;
|
|
309
|
+
}
|
|
301
310
|
if (this.#superseded && !existing) {
|
|
302
|
-
throw new Error(PRODUCT_TEXT.diagnostic(`release ${this.release.releaseId} is no longer
|
|
311
|
+
throw Object.assign(new Error(PRODUCT_TEXT.diagnostic(`release ${this.release.releaseId} is no longer active for new sessions`)), {
|
|
312
|
+
code: "release-superseded",
|
|
313
|
+
});
|
|
303
314
|
}
|
|
304
315
|
if (existing) {
|
|
305
316
|
if (existing.ownerClientId !== clientId
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "darwin",
|
|
6
6
|
"architecture": "arm64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-23T16:42:04.513Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "linux",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-23T16:42:11.967Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "win32",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-23T16:43:16.752Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "051b57f8b1980f1cf2accf7d9f102941e5fcf74b4264f9375d924581e950b0d6",
|
|
12
12
|
"size": 177664
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
package/package.json
CHANGED