@zq-silk/yui 2.21.0 → 2.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/cli/commandCatalog.js +87 -27
- package/dist/cli/dynamicCompletion.js +4 -1
- package/dist/cli/helpRenderer.js +12 -1
- package/dist/cli/interactionPolicy.js +2 -0
- package/dist/cli/invocationRouter.js +5 -5
- package/dist/cli/roleWizard.js +5 -1
- package/dist/cli/updateCommand.js +20 -3
- package/dist/cli/updateOrchestrator.js +13 -0
- package/dist/cli/updatePorts.js +27 -0
- package/dist/commands/globalRoleCommands.js +33 -8
- package/dist/commands/inboxCommands.js +39 -12
- package/dist/commands/operatorCommands.js +2 -5
- package/dist/commands/releaseCommands.js +16 -2
- package/dist/commands/roleConfiguration.js +25 -2
- package/dist/commands/sessionCommands.js +5 -2
- package/dist/commands/taskCommands.js +217 -64
- package/dist/commands/taskContextCommand.js +26 -4
- package/dist/commands/taskFactCommands.js +15 -2
- package/dist/commands/taskIntegrationCommands.js +3 -3
- package/dist/commands/taskRoleRuntimeStatus.js +19 -5
- package/dist/commands/textInput.js +2 -2
- package/dist/context/contextFamilies.js +10 -0
- package/dist/context/currentSkills.js +37 -4
- package/dist/context/runContextPack.js +44 -5
- package/dist/context/runInputContract.js +1 -1
- package/dist/context/sessionBootstrapManifest.js +21 -6
- package/dist/context/sessionSkillNotice.js +74 -0
- package/dist/context/taskContext.js +100 -16
- package/dist/controlPlaneCli.js +17 -7
- package/dist/controller/agentRuntimeObserver.js +57 -12
- package/dist/controller/clientRuntime.js +66 -18
- package/dist/controller/controller.js +19 -2
- package/dist/controller/diagnosticJournal.js +10 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +74 -24
- package/dist/controller/hostLifecycle.js +153 -0
- package/dist/controller/hostLifecycleChecks.js +27 -0
- package/dist/controller/hostUpgrade.js +344 -0
- package/dist/controller/messageHealth.js +4 -3
- package/dist/controller/providerRetryAdmission.js +15 -1
- package/dist/controller/providerRetryDelivery.js +18 -2
- package/dist/controller/runtime.js +82 -21
- package/dist/controller/runtimeEventInbox.js +107 -13
- package/dist/controller/runtimeEventProcessor.js +3 -3
- package/dist/controller/sessionNotify.js +5 -46
- package/dist/controller/taskRunRecovery.js +153 -0
- package/dist/core/controllerClient.js +16 -2
- package/dist/core/controllerServer.js +14 -5
- package/dist/core/controllerTelemetry.js +51 -5
- package/dist/errors/cliFailure.js +14 -5
- package/dist/execution/codexThreadNaming.js +54 -43
- package/dist/executor/agentAdapter.js +32 -4
- package/dist/executor/agentConfigurationFields.js +2 -0
- package/dist/executor/fileRoleLaunchPlanner.js +18 -3
- package/dist/integration/gitIntegrationService.js +3 -2
- package/dist/integration/integrationSourceApplication.js +12 -1
- package/dist/interaction/notificationPresentation.js +11 -16
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/message/messageContinuation.js +3 -1
- package/dist/output/boundedRead.js +36 -7
- package/dist/output/rolePresentation.js +3 -1
- package/dist/runtime/agentEndpoint.js +11 -1
- package/dist/runtime/agentEndpointIdentity.js +2 -0
- package/dist/runtime/agentHost.js +174 -40
- package/dist/runtime/agentHostImplementation.js +3 -1
- package/dist/runtime/codexAppServerRuntime.js +113 -24
- package/dist/runtime/codexInteractiveHost.js +102 -11
- package/dist/runtime/codexNativeAccess.js +1 -1
- package/dist/runtime/contextRecovery.js +15 -0
- package/dist/runtime/hostSessionLifecycle.js +83 -0
- package/dist/runtime/hostUpgradeGate.js +43 -0
- package/dist/runtime/lifecycleChecks.js +47 -0
- package/dist/runtime/lifecycleReservation.js +9 -1
- package/dist/runtime/nativeTitleReceipt.js +31 -0
- package/dist/runtime/nativeTitleSync.js +192 -0
- package/dist/runtime/providerRetry.js +9 -6
- package/dist/runtime/providerRuntimeIdentity.js +1 -1
- package/dist/runtime/structuredProviderHost.js +65 -35
- package/dist/scheduler/wakeupQueue.js +10 -4
- package/dist/storage/sqliteSchema.js +29 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/tmux/tmuxManager.js +4 -2
- package/dist/web/webSettings.js +2 -0
- package/docs/authorization.md +18 -3
- package/docs/cli-information-contract.md +228 -6
- package/docs/cli-information-contract.zh-CN.md +95 -5
- package/docs/managed-turn-and-session-runtime.md +177 -21
- package/docs/managed-turn-and-session-runtime.zh-CN.md +126 -14
- package/docs/provider-retry.md +11 -0
- package/docs/release-workflow.md +65 -0
- package/docs/release-workflow.zh-CN.md +48 -0
- package/docs/roles-and-configuration.md +28 -0
- package/docs/roles-and-configuration.zh-CN.md +21 -0
- package/docs/storage-baseline.md +13 -1
- package/docs/storage-baseline.zh-CN.md +5 -1
- package/docs/task-delivery.md +7 -0
- package/docs/task-delivery.zh-CN.md +5 -0
- package/docs/testing/review-evaluation.md +7 -7
- package/docs/testing/review-evaluation.zh-CN.md +7 -7
- package/docs/testing/verification-levels.md +45 -2
- package/docs/testing/verification-levels.zh-CN.md +21 -2
- package/i18n/README.zh-CN.md +6 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +98 -193
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-leader/references/artifacts.md +41 -0
- package/skills/yui-leader/references/assignment-execution.md +140 -0
- package/skills/yui-leader/references/completion.md +79 -0
- package/skills/yui-leader/references/delivery-scope.md +26 -0
- package/skills/yui-leader/references/execution.md +16 -487
- package/skills/yui-leader/references/implementation.md +81 -0
- package/skills/yui-leader/references/planning.md +7 -45
- package/skills/yui-leader/references/review.md +90 -0
- package/skills/yui-leader/references/task-progress.md +23 -0
- package/skills/yui-leader/references/work-allocation.md +118 -0
- package/skills/yui-operator/SKILL.md +77 -333
- package/skills/yui-operator/VERSION +1 -1
- package/skills/yui-operator/references/body-transport.md +62 -0
- package/skills/yui-operator/references/configuration.md +47 -0
- package/skills/yui-operator/references/lifecycle.md +41 -0
- package/skills/yui-operator/references/progress.md +39 -0
- package/skills/yui-operator/references/routing.md +57 -0
- package/skills/yui-operator/references/submission.md +56 -0
- package/skills/yui-operator/references/task-delivery.md +1 -1
- package/skills/yui-operator/references/web-lifecycle.md +19 -0
- package/skills/yui-operator/scripts/verify-submission.mjs +37 -0
- package/skills/yui-runtime/SKILL.md +182 -531
- package/skills/yui-runtime/VERSION +1 -1
- package/skills/yui-runtime/references/archive.md +51 -0
- package/skills/yui-runtime/references/command-discovery.md +41 -0
- package/skills/yui-runtime/references/completed-followup.md +65 -0
- package/skills/yui-runtime/references/context-ordering.md +3 -34
- package/skills/yui-runtime/references/context-reads.md +61 -0
- package/skills/yui-runtime/references/dispatched-run.md +61 -0
- package/skills/yui-runtime/references/execution-failure.md +105 -0
- package/skills/yui-runtime/references/global-context.md +18 -0
- package/skills/yui-runtime/references/global-input-controls.md +44 -0
- package/skills/yui-runtime/references/host-custody.md +30 -0
- package/skills/yui-runtime/references/inbox-recovery.md +77 -0
- package/skills/yui-runtime/references/incremental-context.md +40 -0
- package/skills/yui-runtime/references/publication.md +4 -111
- package/skills/yui-runtime/references/recovery.md +32 -192
- package/skills/yui-runtime/references/session-identity.md +18 -0
- package/skills/yui-runtime/references/session-replacement.md +45 -0
- package/skills/yui-runtime/references/uncertain-execution.md +29 -0
|
@@ -49,9 +49,10 @@ export class CodexAppServerRuntime {
|
|
|
49
49
|
const result = await this.transport.request("thread/resume", {
|
|
50
50
|
threadId: id,
|
|
51
51
|
...(options.cwd === undefined ? {} : { cwd: text(options.cwd, "Codex thread cwd") }),
|
|
52
|
-
...threadOptions(options)
|
|
52
|
+
...threadOptions(options),
|
|
53
|
+
excludeTurns: true
|
|
53
54
|
});
|
|
54
|
-
return parseThreadSnapshot(result, id, true);
|
|
55
|
+
return this.withLatestTurn(parseThreadSnapshot(result, id, true));
|
|
55
56
|
}
|
|
56
57
|
async setConversationName(input) {
|
|
57
58
|
await this.transport.request("thread/name/set", {
|
|
@@ -63,9 +64,81 @@ export class CodexAppServerRuntime {
|
|
|
63
64
|
const id = text(conversationId, "Codex thread id");
|
|
64
65
|
const result = await this.transport.request("thread/read", {
|
|
65
66
|
threadId: id,
|
|
66
|
-
includeTurns:
|
|
67
|
+
includeTurns: false
|
|
67
68
|
});
|
|
68
|
-
|
|
69
|
+
const snapshot = parseThreadSnapshot(result, id, "unknown");
|
|
70
|
+
return options.includeTurns === false ? snapshot : this.withLatestTurn(snapshot);
|
|
71
|
+
}
|
|
72
|
+
/** Current execution metadata, never an unbounded transcript hydration. */
|
|
73
|
+
async withLatestTurn(snapshot) {
|
|
74
|
+
const page = await this.transport.request("thread/turns/list", {
|
|
75
|
+
threadId: snapshot.threadId, limit: 1, sortDirection: "desc", itemsView: "notLoaded"
|
|
76
|
+
});
|
|
77
|
+
const records = codexPage(page, new Set()).data;
|
|
78
|
+
if (records.length > 1)
|
|
79
|
+
throw new Error("Codex latest Turn page exceeded its requested bound.");
|
|
80
|
+
const turns = records.map(parseTurnSnapshot);
|
|
81
|
+
const latest = turns[0];
|
|
82
|
+
return {
|
|
83
|
+
...snapshot,
|
|
84
|
+
// The metadata response may contain no Turns. Do not carry an old active
|
|
85
|
+
// identity over a separately observed latest Turn.
|
|
86
|
+
activeTurnId: latest?.status === "inProgress" ? latest.turnId : undefined,
|
|
87
|
+
latestTurnStatus: latest?.status,
|
|
88
|
+
turns
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Recover only the persisted owned Turn. Scan metadata, then its items,
|
|
92
|
+
* newest first, stopping at the last nonempty assistant message. A malformed
|
|
93
|
+
* page or oversized individual item remains an error, never guessed success. */
|
|
94
|
+
async readTurn(conversationId, turnId) {
|
|
95
|
+
const threadId = text(conversationId, "Codex thread id");
|
|
96
|
+
const expectedTurnId = text(turnId, "Codex owned Turn id");
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
let cursor;
|
|
99
|
+
for (;;) {
|
|
100
|
+
const page = codexPage(await this.transport.request("thread/turns/list", {
|
|
101
|
+
threadId, limit: 50, sortDirection: "desc", itemsView: "notLoaded",
|
|
102
|
+
...(cursor === undefined ? {} : { cursor })
|
|
103
|
+
}), seen);
|
|
104
|
+
if (page.data.length > 50)
|
|
105
|
+
throw new Error("Codex Turn page exceeded its requested bound.");
|
|
106
|
+
const found = page.data.map(parseTurnSnapshot).find(turn => turn.turnId === expectedTurnId);
|
|
107
|
+
if (found !== undefined) {
|
|
108
|
+
if (found.status === "inProgress")
|
|
109
|
+
return found;
|
|
110
|
+
const output = await this.readTurnOutput(threadId, expectedTurnId);
|
|
111
|
+
return { ...found, ...(output === undefined ? {} : { output }) };
|
|
112
|
+
}
|
|
113
|
+
if (page.nextCursor === null)
|
|
114
|
+
return undefined;
|
|
115
|
+
cursor = page.nextCursor;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async readTurnOutput(threadId, turnId) {
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
let cursor;
|
|
121
|
+
for (;;) {
|
|
122
|
+
const page = codexPage(await this.transport.request("thread/items/list", {
|
|
123
|
+
threadId, turnId, limit: 1, sortDirection: "desc",
|
|
124
|
+
...(cursor === undefined ? {} : { cursor })
|
|
125
|
+
}), seen);
|
|
126
|
+
if (page.data.length > 1)
|
|
127
|
+
throw new Error("Codex item page exceeded its requested bound.");
|
|
128
|
+
for (const value of page.data) {
|
|
129
|
+
const entry = object(value);
|
|
130
|
+
const item = object(entry?.item);
|
|
131
|
+
if (entry?.turnId !== turnId || item === null) {
|
|
132
|
+
throw new Error("Codex item page does not belong to the owned Turn.");
|
|
133
|
+
}
|
|
134
|
+
if (item.type === "agentMessage" && typeof item.text === "string" && item.text.trim().length > 0) {
|
|
135
|
+
return item.text;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (page.nextCursor === null)
|
|
139
|
+
return undefined;
|
|
140
|
+
cursor = page.nextCursor;
|
|
141
|
+
}
|
|
69
142
|
}
|
|
70
143
|
async readGoal(conversationId) {
|
|
71
144
|
const id = text(conversationId, "Codex thread id");
|
|
@@ -98,6 +171,7 @@ export class CodexAppServerRuntime {
|
|
|
98
171
|
return this.startTurn({
|
|
99
172
|
conversationId: input.conversationId,
|
|
100
173
|
text: input.text,
|
|
174
|
+
...(input.additionalContext === undefined ? {} : { additionalContext: input.additionalContext }),
|
|
101
175
|
expectedNoActiveTurn: input.expectedNoActiveTurn,
|
|
102
176
|
...(input.requireQuiescent ? { requireQuiescent: true } : {}),
|
|
103
177
|
...(input.expectedFailedNativeTurnId === undefined ? {} : {
|
|
@@ -157,6 +231,7 @@ export class CodexAppServerRuntime {
|
|
|
157
231
|
try {
|
|
158
232
|
const result = await this.transport.request("turn/start", {
|
|
159
233
|
threadId,
|
|
234
|
+
...(input.additionalContext === undefined ? {} : { additionalContext: input.additionalContext }),
|
|
160
235
|
...(input.clientUserMessageId === undefined
|
|
161
236
|
? {}
|
|
162
237
|
: { clientUserMessageId: text(input.clientUserMessageId, "Codex input attempt id") }),
|
|
@@ -192,6 +267,7 @@ export class CodexAppServerRuntime {
|
|
|
192
267
|
try {
|
|
193
268
|
const result = await this.transport.request("turn/steer", {
|
|
194
269
|
threadId,
|
|
270
|
+
...(input.additionalContext === undefined ? {} : { additionalContext: input.additionalContext }),
|
|
195
271
|
expectedTurnId,
|
|
196
272
|
...(input.clientUserMessageId === undefined
|
|
197
273
|
? {}
|
|
@@ -330,26 +406,7 @@ function parseThreadSnapshot(result, expectedThreadId, loaded) {
|
|
|
330
406
|
throw new Error("Codex thread status is missing or unsupported.");
|
|
331
407
|
if (!Array.isArray(thread.turns))
|
|
332
408
|
throw new Error("Codex thread turns are missing or invalid.");
|
|
333
|
-
const turnSnapshots = thread.turns.map(
|
|
334
|
-
const turn = object(value);
|
|
335
|
-
if (turn === null)
|
|
336
|
-
throw new Error("Codex thread contains an invalid Turn.");
|
|
337
|
-
const turnId = text(optionalId(turn.id), "Codex response Turn id");
|
|
338
|
-
const status = optionalTurnStatus(turn.status);
|
|
339
|
-
if (status === undefined)
|
|
340
|
-
throw new Error("Codex Turn status is missing or unsupported.");
|
|
341
|
-
const output = codexTurnOutput(turn);
|
|
342
|
-
const error = providerError(turn.error);
|
|
343
|
-
return {
|
|
344
|
-
turnId,
|
|
345
|
-
status,
|
|
346
|
-
...(output === undefined ? {} : { output }),
|
|
347
|
-
...(error === undefined ? {} : { error }),
|
|
348
|
-
...(turn.error === undefined || turn.error === null ? {} : {
|
|
349
|
-
rawError: serializeAgentErrorRaw(turn.error)
|
|
350
|
-
})
|
|
351
|
-
};
|
|
352
|
-
});
|
|
409
|
+
const turnSnapshots = thread.turns.map(parseTurnSnapshot);
|
|
353
410
|
const active = [...turnSnapshots].reverse().find((turn) => turn.status === "inProgress");
|
|
354
411
|
const latestStatus = turnSnapshots.at(-1)?.status;
|
|
355
412
|
return {
|
|
@@ -365,6 +422,38 @@ function parseThreadSnapshot(result, expectedThreadId, loaded) {
|
|
|
365
422
|
raw: result
|
|
366
423
|
};
|
|
367
424
|
}
|
|
425
|
+
function parseTurnSnapshot(value) {
|
|
426
|
+
const turn = object(value);
|
|
427
|
+
if (turn === null)
|
|
428
|
+
throw new Error("Codex thread contains an invalid Turn.");
|
|
429
|
+
const turnId = text(optionalId(turn.id), "Codex response Turn id");
|
|
430
|
+
const status = optionalTurnStatus(turn.status);
|
|
431
|
+
if (status === undefined)
|
|
432
|
+
throw new Error("Codex Turn status is missing or unsupported.");
|
|
433
|
+
const output = codexTurnOutput(turn);
|
|
434
|
+
const error = providerError(turn.error);
|
|
435
|
+
return {
|
|
436
|
+
turnId,
|
|
437
|
+
status,
|
|
438
|
+
...(output === undefined ? {} : { output }),
|
|
439
|
+
...(error === undefined ? {} : { error }),
|
|
440
|
+
...(turn.error === undefined || turn.error === null ? {} : {
|
|
441
|
+
rawError: serializeAgentErrorRaw(turn.error)
|
|
442
|
+
})
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function codexPage(page, seen) {
|
|
446
|
+
if (!Array.isArray(page.data)
|
|
447
|
+
|| !(page.nextCursor === null || typeof page.nextCursor === "string" && page.nextCursor.length > 0)) {
|
|
448
|
+
throw new Error("Codex history page is missing data or its continuation boundary.");
|
|
449
|
+
}
|
|
450
|
+
if (typeof page.nextCursor === "string") {
|
|
451
|
+
if (seen.has(page.nextCursor))
|
|
452
|
+
throw new Error("Codex history cursor repeated.");
|
|
453
|
+
seen.add(page.nextCursor);
|
|
454
|
+
}
|
|
455
|
+
return { data: page.data, nextCursor: page.nextCursor };
|
|
456
|
+
}
|
|
368
457
|
/** Extract the last assistant message from one native Codex Turn without rewriting it. */
|
|
369
458
|
export function codexTurnOutput(turn) {
|
|
370
459
|
for (const value of [...arrayMember(turn, "items")].reverse()) {
|
|
@@ -4,6 +4,11 @@ import { once } from "node:events";
|
|
|
4
4
|
import WebSocket, { WebSocketServer } from "ws";
|
|
5
5
|
import { AGENT_HOST_CONTROL_PROTOCOL } from "./agentHostProtocol.js";
|
|
6
6
|
import { openCodexInteractiveConnection } from "./structuredProviderHost.js";
|
|
7
|
+
import { createSessionSkillNotice, createSessionSkillLifecycleCheck } from "../context/sessionSkillNotice.js";
|
|
8
|
+
import { isNativeContextRecovery } from "./contextRecovery.js";
|
|
9
|
+
import { createHostSessionLifecycle } from "./hostSessionLifecycle.js";
|
|
10
|
+
import { createNonBlockingChecks } from "./lifecycleChecks.js";
|
|
11
|
+
import { createNativeTitleSync } from "./nativeTitleSync.js";
|
|
7
12
|
/**
|
|
8
13
|
* Keep the native TUI and its transparent App Server attachment in one pane.
|
|
9
14
|
* The existing Host acknowledgement carries the ID from that TUI's exact
|
|
@@ -15,6 +20,8 @@ import { openCodexInteractiveConnection } from "./structuredProviderHost.js";
|
|
|
15
20
|
*/
|
|
16
21
|
export async function runCodexInteractiveHost(home, payload, openAgentHostControl) {
|
|
17
22
|
const environment = payload.environment;
|
|
23
|
+
const skillNotice = createSessionSkillNotice(home);
|
|
24
|
+
const pendingNotices = new Map();
|
|
18
25
|
if (environment.YUI_SESSION_SCOPE !== "global" || environment.YUI_ADAPTER_ID !== "codex"
|
|
19
26
|
|| payload.providerControl !== undefined) {
|
|
20
27
|
throw new Error("Interactive Codex Host requires a global native TUI launch.");
|
|
@@ -28,8 +35,19 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
|
|
|
28
35
|
throw new Error("Codex proxy base arguments are invalid.");
|
|
29
36
|
}
|
|
30
37
|
const expectedId = environment.YUI_NATIVE_SESSION_ID;
|
|
38
|
+
const lifecycleChecks = createNonBlockingChecks([createSessionSkillLifecycleCheck(home, () => environment)], error => process.stderr.write(`Session lifecycle check failed: ${String(error)}\n`));
|
|
39
|
+
const lifecycle = createHostSessionLifecycle({ after: lifecycleChecks });
|
|
40
|
+
lifecycle.send({ type: "launch" });
|
|
31
41
|
let snapshot = {
|
|
32
|
-
schemaVersion: 1, state:
|
|
42
|
+
schemaVersion: 1, state: lifecycle.state, adapterId: "codex", updatedAt: new Date().toISOString()
|
|
43
|
+
};
|
|
44
|
+
const transition = (event, fields = {}) => {
|
|
45
|
+
const result = lifecycle.send(event);
|
|
46
|
+
if (result.status === "ignored")
|
|
47
|
+
return;
|
|
48
|
+
if (result.status === "invalid")
|
|
49
|
+
throw new Error(result.reason);
|
|
50
|
+
snapshot = { ...snapshot, ...fields, state: result.state, updatedAt: new Date().toISOString() };
|
|
33
51
|
};
|
|
34
52
|
const control = await openAgentHostControl(home, payload, () => snapshot, async (request) => {
|
|
35
53
|
if (request.type !== "status") {
|
|
@@ -41,6 +59,9 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
|
|
|
41
59
|
let client;
|
|
42
60
|
let closing = false;
|
|
43
61
|
let startupRequestId;
|
|
62
|
+
let initializeRequestId;
|
|
63
|
+
let nativeAccountHome;
|
|
64
|
+
let titleSync;
|
|
44
65
|
let connection;
|
|
45
66
|
let relay;
|
|
46
67
|
const signals = ["SIGTERM", "SIGHUP", "SIGINT"];
|
|
@@ -48,11 +69,9 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
|
|
|
48
69
|
const fail = (error) => {
|
|
49
70
|
if (closing || snapshot.state === "failed")
|
|
50
71
|
return;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
updatedAt: new Date().toISOString()
|
|
55
|
-
};
|
|
72
|
+
transition({ type: "failure", outcome: "failed" }, {
|
|
73
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
74
|
+
});
|
|
56
75
|
process.stderr.write(`Yui Codex attachment failed: ${snapshot.detail}\n`);
|
|
57
76
|
client?.terminate();
|
|
58
77
|
stop();
|
|
@@ -91,6 +110,11 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
|
|
|
91
110
|
if (binary)
|
|
92
111
|
throw new Error("Codex TUI sent a binary App Server request.");
|
|
93
112
|
const message = jsonObject(JSON.parse(data.toString()));
|
|
113
|
+
if (message.method === "thread/name/set")
|
|
114
|
+
titleSync?.close();
|
|
115
|
+
if (message.method === "initialize"
|
|
116
|
+
&& (typeof message.id === "string" || typeof message.id === "number"))
|
|
117
|
+
initializeRequestId = message.id;
|
|
94
118
|
if (snapshot.nativeSessionId === undefined
|
|
95
119
|
&& (message.method === "thread/start" || message.method === "thread/resume")) {
|
|
96
120
|
if (startupRequestId !== undefined)
|
|
@@ -106,6 +130,23 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
|
|
|
106
130
|
startupRequestId = message.id;
|
|
107
131
|
message.params = codexInteractiveStartupParameters(payload, params);
|
|
108
132
|
}
|
|
133
|
+
if (message.method === "turn/start" || message.method === "turn/steer") {
|
|
134
|
+
if (snapshot.nativeSessionId === undefined)
|
|
135
|
+
throw new Error("Skill check requires the reserved native Thread.");
|
|
136
|
+
if (typeof message.id !== "string" && typeof message.id !== "number"
|
|
137
|
+
|| pendingNotices.has(message.id))
|
|
138
|
+
throw new Error("Turn input requires a unique correlation id.");
|
|
139
|
+
const params = jsonObject(message.params);
|
|
140
|
+
if (params.threadId !== snapshot.nativeSessionId)
|
|
141
|
+
throw new Error("Turn input targets another Thread.");
|
|
142
|
+
const prepared = codexInteractiveSkillInput(skillNotice, environment, snapshot.nativeSessionId, params);
|
|
143
|
+
message.params = prepared.params;
|
|
144
|
+
pendingNotices.set(message.id, {
|
|
145
|
+
commit: prepared.commit,
|
|
146
|
+
...(message.method === "turn/steer" && typeof params.expectedTurnId === "string"
|
|
147
|
+
? { expectedTurnId: params.expectedTurnId } : {})
|
|
148
|
+
});
|
|
149
|
+
}
|
|
109
150
|
void connection.send(message).catch(fail);
|
|
110
151
|
}
|
|
111
152
|
catch (error) {
|
|
@@ -115,6 +156,30 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
|
|
|
115
156
|
});
|
|
116
157
|
connection.onMessage((message) => {
|
|
117
158
|
try {
|
|
159
|
+
if (snapshot.nativeSessionId !== undefined
|
|
160
|
+
&& isNativeContextRecovery("codex", snapshot.nativeSessionId, message))
|
|
161
|
+
skillNotice.invalidate();
|
|
162
|
+
if (message.method === "thread/name/updated"
|
|
163
|
+
&& message.params?.threadId === snapshot.nativeSessionId)
|
|
164
|
+
titleSync?.close();
|
|
165
|
+
if (message.method === undefined && message.id === initializeRequestId) {
|
|
166
|
+
const initialized = message.result;
|
|
167
|
+
nativeAccountHome = typeof initialized?.codexHome === "string" ? initialized.codexHome : undefined;
|
|
168
|
+
initializeRequestId = undefined;
|
|
169
|
+
}
|
|
170
|
+
if (message.method === undefined && (typeof message.id === "string" || typeof message.id === "number")) {
|
|
171
|
+
const pending = pendingNotices.get(message.id);
|
|
172
|
+
if (pending !== undefined) {
|
|
173
|
+
pendingNotices.delete(message.id);
|
|
174
|
+
const result = message.result;
|
|
175
|
+
const turnId = result?.turn?.id ?? result?.turnId;
|
|
176
|
+
if (message.error === undefined && typeof turnId === "string" && turnId.length > 0
|
|
177
|
+
&& (pending.expectedTurnId === undefined || pending.expectedTurnId === turnId)) {
|
|
178
|
+
pending.commit();
|
|
179
|
+
titleSync?.notify();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
118
183
|
let nativeSessionId;
|
|
119
184
|
if (startupRequestId !== undefined && message.id === startupRequestId
|
|
120
185
|
&& message.method === undefined) {
|
|
@@ -135,13 +200,17 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
|
|
|
135
200
|
fail(error);
|
|
136
201
|
return;
|
|
137
202
|
}
|
|
138
|
-
if (nativeSessionId === undefined || snapshot.state === "failed")
|
|
203
|
+
if (nativeSessionId === undefined || closing || snapshot.state === "failed")
|
|
139
204
|
return;
|
|
140
205
|
startupRequestId = undefined;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
206
|
+
// Ready here means the native TUI attachment is ready, not a managed
|
|
207
|
+
// AgentRun acceptance or proof that the native Thread is idle.
|
|
208
|
+
transition({ type: "interactive-connected", nativeSessionId }, {
|
|
209
|
+
nativeSessionId, conversationId: nativeSessionId
|
|
210
|
+
});
|
|
211
|
+
titleSync = createNativeTitleSync(payload, nativeSessionId, nativeAccountHome, result => process.stderr.write(`Native title: ${JSON.stringify(result)}\n`));
|
|
212
|
+
if (expectedId !== undefined)
|
|
213
|
+
titleSync.notify();
|
|
145
214
|
});
|
|
146
215
|
}
|
|
147
216
|
catch (error) {
|
|
@@ -165,6 +234,7 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
|
|
|
165
234
|
for (const signal of signals)
|
|
166
235
|
process.on(signal, stop);
|
|
167
236
|
const [code] = await once(child, "exit");
|
|
237
|
+
transition({ type: "exit" });
|
|
168
238
|
return typeof code === "number" ? code : 1;
|
|
169
239
|
}
|
|
170
240
|
catch (error) {
|
|
@@ -172,7 +242,9 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
|
|
|
172
242
|
throw error;
|
|
173
243
|
}
|
|
174
244
|
finally {
|
|
245
|
+
lifecycleChecks.close();
|
|
175
246
|
closing = true;
|
|
247
|
+
titleSync?.close();
|
|
176
248
|
for (const signal of signals)
|
|
177
249
|
process.removeListener(signal, stop);
|
|
178
250
|
stop();
|
|
@@ -183,6 +255,25 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
|
|
|
183
255
|
await control.close();
|
|
184
256
|
}
|
|
185
257
|
}
|
|
258
|
+
/** Keep application metadata out of native user input, including on steer.
|
|
259
|
+
* Formal titles are synchronized separately; no automatic-title heuristic is assumed. */
|
|
260
|
+
export function codexInteractiveSkillInput(notice, environment, nativeSessionId, params) {
|
|
261
|
+
if (!Array.isArray(params.input))
|
|
262
|
+
throw new Error("Turn input must be an array.");
|
|
263
|
+
const prepared = notice.prepare("", environment, nativeSessionId);
|
|
264
|
+
return {
|
|
265
|
+
params: {
|
|
266
|
+
...params,
|
|
267
|
+
...(prepared.context === undefined ? {} : {
|
|
268
|
+
additionalContext: {
|
|
269
|
+
...(params.additionalContext == null ? {} : jsonObject(params.additionalContext)),
|
|
270
|
+
"yui.session-skills": { kind: "application", value: prepared.context }
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
},
|
|
274
|
+
commit: prepared.commit
|
|
275
|
+
};
|
|
276
|
+
}
|
|
186
277
|
/** The remote TUI can omit local launch configuration. Apply the reserved
|
|
187
278
|
* workspace and managed identity on its actual startup, never by starting a
|
|
188
279
|
* second Thread or changing daemon-wide configuration. */
|
|
@@ -5,7 +5,7 @@ const READS = new Set([
|
|
|
5
5
|
"config/read", "configRequirements/read", "model/list", "collaborationMode/list",
|
|
6
6
|
"experimentalFeature/list", "skills/list", "hooks/list", "mcpServerStatus/list", "account/read",
|
|
7
7
|
"account/rateLimits/read", "modelProvider/capabilities/read", "permissionProfile/list",
|
|
8
|
-
"thread/read", "thread/turns/list", "thread/items/list", "thread/goal/get"
|
|
8
|
+
"thread/read", "thread/turns/list", "thread/items/list", "thread/goal/get", "thread/backgroundTerminals/list"
|
|
9
9
|
]);
|
|
10
10
|
const REQUESTS = new Set([
|
|
11
11
|
"item/commandExecution/requestApproval", "item/fileChange/requestApproval",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Only explicit, identity-bound native lifecycle events invalidate context.
|
|
2
|
+
* Absence of an event never proves that instructions are still in memory. */
|
|
3
|
+
export function isNativeContextRecovery(adapter, nativeSessionId, message) {
|
|
4
|
+
if (adapter === "claude") {
|
|
5
|
+
return message.type === "system" && message.subtype === "compact_boundary"
|
|
6
|
+
&& message.session_id === nativeSessionId;
|
|
7
|
+
}
|
|
8
|
+
const params = message.params;
|
|
9
|
+
if (!params || params.threadId !== nativeSessionId)
|
|
10
|
+
return false;
|
|
11
|
+
if (message.method === "thread/compacted")
|
|
12
|
+
return true;
|
|
13
|
+
const item = params.item;
|
|
14
|
+
return message.method === "item/completed" && item?.type === "contextCompaction";
|
|
15
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const live = [
|
|
2
|
+
"idle", "starting", "ready", "busy", "settling", "delivery-unknown", "rejected", "failed"
|
|
3
|
+
];
|
|
4
|
+
const available = ["idle", "busy", "rejected", "failed"];
|
|
5
|
+
const occupancy = (value) => (event) => "occupancy" in event && event.occupancy === value;
|
|
6
|
+
/** Lifecycle only: no version checks, upgrade phases, I/O or business policy.
|
|
7
|
+
* Each event selects one guarded edge; callers cannot assign a target state.
|
|
8
|
+
* Existing authority/admission checks must succeed before sending commands. */
|
|
9
|
+
export const HOST_SESSION_LIFECYCLE_TRANSITIONS = {
|
|
10
|
+
launch: [{ from: [...available, "exited"], to: "starting" }],
|
|
11
|
+
submit: [{ from: available, to: "starting" }],
|
|
12
|
+
connected: [
|
|
13
|
+
{ from: live, guard: occupancy("owned"), to: "ready" },
|
|
14
|
+
{ from: live, guard: occupancy("unknown"), to: "delivery-unknown" },
|
|
15
|
+
{ from: live, guard: occupancy("busy"), to: "busy" },
|
|
16
|
+
{ from: live, guard: occupancy("idle"), to: "idle" }
|
|
17
|
+
],
|
|
18
|
+
accepted: [{ from: ["starting", "ready", "delivery-unknown"], to: "ready" }],
|
|
19
|
+
"interactive-connected": [{ from: ["starting", "ready"], to: "ready" }],
|
|
20
|
+
"native-started": [{ from: live, to: "busy" }],
|
|
21
|
+
terminal: [{ from: ["starting", "ready", "busy", "delivery-unknown", "settling", "failed"], to: "settling" }],
|
|
22
|
+
settled: [
|
|
23
|
+
{ from: live, guard: occupancy("idle"), to: "idle" },
|
|
24
|
+
{ from: live, guard: occupancy("busy"), to: "busy" }
|
|
25
|
+
],
|
|
26
|
+
disconnected: [{ from: live, to: "starting" }],
|
|
27
|
+
"authority-changed": [{ from: available, to: "idle" }],
|
|
28
|
+
failure: ["busy", "delivery-unknown", "rejected", "failed"].map(outcome => ({
|
|
29
|
+
from: live, guard: event => event.type === "failure" && event.outcome === outcome, to: outcome
|
|
30
|
+
})),
|
|
31
|
+
exit: [{ from: live, to: "exited" }]
|
|
32
|
+
};
|
|
33
|
+
export function createHostSessionLifecycle(hooks = {}) {
|
|
34
|
+
let state = "idle";
|
|
35
|
+
let revision = 0;
|
|
36
|
+
let identity = {};
|
|
37
|
+
const edgeFor = (event) => HOST_SESSION_LIFECYCLE_TRANSITIONS[event.type].find(rule => rule.from.includes(state) && (!rule.guard || rule.guard(event)));
|
|
38
|
+
return {
|
|
39
|
+
get state() { return state; },
|
|
40
|
+
get revision() { return revision; },
|
|
41
|
+
/** State eligibility only; caller still owns exact writer/resource guards. */
|
|
42
|
+
can(event) { return edgeFor(event) !== undefined; },
|
|
43
|
+
send(input) {
|
|
44
|
+
const event = Object.freeze({ ...input });
|
|
45
|
+
if (state === "exited" && event.type !== "launch")
|
|
46
|
+
return { status: "ignored", state, reason: "exited" };
|
|
47
|
+
if (!["launch", "connected"].includes(event.type)) {
|
|
48
|
+
for (const key of ["processInstanceId", "nativeSessionId"]) {
|
|
49
|
+
if (identity[key] && event[key] && identity[key] !== event[key]) {
|
|
50
|
+
return { status: "ignored", state, reason: `stale-${key}` };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (["accepted", "terminal"].includes(event.type)
|
|
54
|
+
&& identity.attemptId && event.attemptId && identity.attemptId !== event.attemptId) {
|
|
55
|
+
return { status: "ignored", state, reason: "stale-attempt" };
|
|
56
|
+
}
|
|
57
|
+
if (event.type === "terminal" && identity.nativeTurnId && event.nativeTurnId
|
|
58
|
+
&& identity.nativeTurnId !== event.nativeTurnId) {
|
|
59
|
+
return { status: "ignored", state, reason: "stale-turn" };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const edge = edgeFor(event);
|
|
63
|
+
if (!edge)
|
|
64
|
+
return { status: "invalid", state, reason: `No ${event.type} edge from ${state}` };
|
|
65
|
+
const transition = Object.freeze({ from: state, to: edge.to, revision: revision + 1, event });
|
|
66
|
+
hooks.before?.notify(transition);
|
|
67
|
+
state = edge.to;
|
|
68
|
+
revision += 1;
|
|
69
|
+
// A new authorized attachment must not inherit the departed proxy's
|
|
70
|
+
// identity fence. A launch reusing a live connection supplies its IDs.
|
|
71
|
+
if (event.type === "launch")
|
|
72
|
+
identity = {};
|
|
73
|
+
identity = {
|
|
74
|
+
processInstanceId: event.processInstanceId ?? identity.processInstanceId,
|
|
75
|
+
nativeSessionId: event.nativeSessionId ?? identity.nativeSessionId,
|
|
76
|
+
attemptId: event.attemptId,
|
|
77
|
+
nativeTurnId: event.nativeTurnId
|
|
78
|
+
};
|
|
79
|
+
hooks.after?.notify(transition);
|
|
80
|
+
return { status: "applied", state };
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Host-local input admission fence. The existing lifecycle mailbox owns the
|
|
2
|
+
* durable request; this gate owns only the actual loaded process's drain. */
|
|
3
|
+
export function createHostUpgradeGate() {
|
|
4
|
+
let current;
|
|
5
|
+
let draining;
|
|
6
|
+
return {
|
|
7
|
+
get observation() { return current; },
|
|
8
|
+
get fenced() { return current !== undefined; },
|
|
9
|
+
request(fence, identity) {
|
|
10
|
+
if (fence.nativeSessionId !== identity.nativeSessionId
|
|
11
|
+
|| fence.processInstanceId !== identity.processInstanceId
|
|
12
|
+
|| fence.hostGeneration !== identity.hostGeneration)
|
|
13
|
+
throw new Error("Host upgrade identity changed.");
|
|
14
|
+
if (current && (fence.requestId !== current.requestId || fence.nativeSessionId !== current.nativeSessionId
|
|
15
|
+
|| fence.processInstanceId !== current.processInstanceId || fence.hostGeneration !== current.hostGeneration)) {
|
|
16
|
+
throw new Error("Host already holds a different upgrade request.");
|
|
17
|
+
}
|
|
18
|
+
current ??= { requestId: fence.requestId, nativeSessionId: fence.nativeSessionId,
|
|
19
|
+
processInstanceId: fence.processInstanceId, hostGeneration: fence.hostGeneration, state: "waiting" };
|
|
20
|
+
},
|
|
21
|
+
async drain(input) {
|
|
22
|
+
if (!current || current.state !== "waiting")
|
|
23
|
+
return;
|
|
24
|
+
if (input.busy || input.events.pending !== 0 || input.events.pendingTerminals !== 0
|
|
25
|
+
|| input.events.unpersisted !== 0 || input.events.failure !== undefined)
|
|
26
|
+
return;
|
|
27
|
+
draining ??= (async () => {
|
|
28
|
+
try {
|
|
29
|
+
if (!await input.checkQuiescent())
|
|
30
|
+
return;
|
|
31
|
+
await input.release();
|
|
32
|
+
current = { ...current, state: "drained" };
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
current = { ...current, state: "failed",
|
|
36
|
+
detail: error instanceof Error ? error.message : String(error) };
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
await draining;
|
|
40
|
+
draining = undefined;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function createNonBlockingChecks(checks, onError = () => { }) {
|
|
2
|
+
if (new Set(checks.map(check => check.id)).size !== checks.length)
|
|
3
|
+
throw new Error("Duplicate lifecycle check id.");
|
|
4
|
+
const abort = new AbortController();
|
|
5
|
+
const lanes = checks.map(check => ({ check, running: false, pending: undefined }));
|
|
6
|
+
const launch = (lane) => {
|
|
7
|
+
if (lane.running || abort.signal.aborted)
|
|
8
|
+
return;
|
|
9
|
+
lane.running = true;
|
|
10
|
+
// Defer even synchronous callbacks until the caller has committed its state.
|
|
11
|
+
void Promise.resolve().then(async () => {
|
|
12
|
+
while (lane.pending && !abort.signal.aborted) {
|
|
13
|
+
const { value } = lane.pending;
|
|
14
|
+
lane.pending = undefined;
|
|
15
|
+
try {
|
|
16
|
+
await lane.check.run(value, abort.signal);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
try {
|
|
20
|
+
onError(error);
|
|
21
|
+
}
|
|
22
|
+
catch { /* Diagnostics cannot gate lifecycle. */ }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}).finally(() => {
|
|
26
|
+
lane.running = false;
|
|
27
|
+
if (lane.pending)
|
|
28
|
+
launch(lane);
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
notify(observation) {
|
|
33
|
+
if (abort.signal.aborted)
|
|
34
|
+
return;
|
|
35
|
+
for (const lane of lanes) {
|
|
36
|
+
lane.pending = { value: observation };
|
|
37
|
+
launch(lane);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
get pendingCount() { return lanes.filter(lane => lane.pending !== undefined).length; },
|
|
41
|
+
close() {
|
|
42
|
+
abort.abort();
|
|
43
|
+
for (const lane of lanes)
|
|
44
|
+
lane.pending = undefined;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -3,6 +3,9 @@ export const RUNTIME_LIFECYCLE_OWNER = "runtime-lifecycle";
|
|
|
3
3
|
export const RUNTIME_CLEANUP_REQUIRED_REASON = "runtime-cleanup-required";
|
|
4
4
|
export const RUNTIME_HOST_DETACH_REQUIRED_REASON = "runtime-host-detach-required";
|
|
5
5
|
export const RUNTIME_SESSION_REPLACE_REQUIRED_REASON = "runtime-session-replace-required";
|
|
6
|
+
export const RUNTIME_HOST_UPGRADE_REASON = "runtime-host-upgrade-required";
|
|
7
|
+
export const RUNTIME_HOST_UPGRADE_DRAINED = "runtime-host-upgrade-drained";
|
|
8
|
+
export const RUNTIME_HOST_UPGRADE_FAILED = "runtime-host-upgrade-failed";
|
|
6
9
|
/**
|
|
7
10
|
* A Role runtime lifecycle lane already holds an explicit cleanup obligation.
|
|
8
11
|
* This is scheduler
|
|
@@ -48,12 +51,17 @@ export function runtimeCleanupDisposition(mailbox) {
|
|
|
48
51
|
return "end-session";
|
|
49
52
|
if (reasons.includes(RUNTIME_HOST_DETACH_REQUIRED_REASON))
|
|
50
53
|
return "detach-host";
|
|
54
|
+
if (reasons.includes(RUNTIME_HOST_UPGRADE_REASON))
|
|
55
|
+
return "upgrade-host";
|
|
51
56
|
return null;
|
|
52
57
|
}
|
|
53
58
|
export function isRuntimeCleanupReason(reason) {
|
|
54
59
|
return reason === RUNTIME_CLEANUP_REQUIRED_REASON
|
|
55
60
|
|| reason === RUNTIME_SESSION_REPLACE_REQUIRED_REASON
|
|
56
|
-
|| reason === RUNTIME_HOST_DETACH_REQUIRED_REASON
|
|
61
|
+
|| reason === RUNTIME_HOST_DETACH_REQUIRED_REASON
|
|
62
|
+
|| reason === RUNTIME_HOST_UPGRADE_REASON
|
|
63
|
+
|| reason === RUNTIME_HOST_UPGRADE_DRAINED
|
|
64
|
+
|| reason === RUNTIME_HOST_UPGRADE_FAILED;
|
|
57
65
|
}
|
|
58
66
|
export function hasRuntimeLifecycleWork(mailbox) {
|
|
59
67
|
return mailbox !== null && mailboxHasWork(mailbox);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export function nativeTitleDirectory(home, identity) {
|
|
5
|
+
const key = createHash("sha256").update(JSON.stringify([
|
|
6
|
+
identity.scope, identity.taskId ?? null, identity.roleName, identity.agentId, identity.threadId
|
|
7
|
+
])).digest("hex");
|
|
8
|
+
return join(home, "runtime", "native-titles", key);
|
|
9
|
+
}
|
|
10
|
+
export function parseNativeTitleReceipt(text, identity) {
|
|
11
|
+
const value = JSON.parse(text);
|
|
12
|
+
if (value.schemaVersion !== 1 || JSON.stringify(value.identity) !== JSON.stringify(identity)
|
|
13
|
+
|| !value.accountHome || !value.attemptId || !value.requestedTitle
|
|
14
|
+
|| !(value.expectedTitle === null || typeof value.expectedTitle === "string")
|
|
15
|
+
|| !["submitted", "confirmed", "unknown", "preserved"].includes(value.status)
|
|
16
|
+
|| !Array.isArray(value.generatedTitles) || value.generatedTitles.some(title => typeof title !== "string")) {
|
|
17
|
+
throw new Error("Invalid native title receipt; preserve it for diagnosis.");
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
/** Read-only diagnostics can reuse confirmation, never a submitted naming intent.
|
|
22
|
+
* Unavailable/uncertain evidence supplies no ownership of an existing title. */
|
|
23
|
+
export function confirmedNativeTitles(home, identity) {
|
|
24
|
+
try {
|
|
25
|
+
const receipt = parseNativeTitleReceipt(readFileSync(join(nativeTitleDirectory(home, identity), "receipt.json"), "utf8"), identity);
|
|
26
|
+
return receipt.status === "confirmed" ? receipt.generatedTitles : [];
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|