@cydm/happy-elves 0.1.0-beta.489 → 0.1.0-beta.490
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/apps/cli/package.json +1 -1
- package/apps/daemon/dist/relay/prompt-admission-store.js +5 -1
- package/apps/daemon/dist/session/native-history-refresh.js +4 -1
- package/apps/daemon/dist/session/prompt.js +19 -1
- package/apps/daemon/package.json +1 -1
- package/package.json +1 -1
- package/packages/headless-runtime/dist/claude-provider.js +2 -2
- package/packages/headless-runtime/dist/codex-provider.js +2 -2
- package/packages/headless-runtime/dist/core.d.ts +1 -0
- package/packages/headless-runtime/dist/core.js +34 -0
- package/packages/headless-runtime/dist/index.js +1 -0
- package/packages/runtime/dist/headless.d.ts +7 -0
package/apps/cli/package.json
CHANGED
|
@@ -177,10 +177,14 @@ export async function waitForInboundPromptStart(requestId, timeoutMs) {
|
|
|
177
177
|
}
|
|
178
178
|
export function retryablePreStartPromptError(code) {
|
|
179
179
|
// Internal queue/schedule admission can reset deterministic pre-start
|
|
180
|
-
// failures because no runtime turn was durably materialized.
|
|
180
|
+
// failures because no runtime turn was durably materialized. A queue gate
|
|
181
|
+
// reports CREW_SESSION_BUSY before the provider is touched; retaining that
|
|
182
|
+
// terminal response under the stable queue prompt request id would make the
|
|
183
|
+
// same FIFO head replay its own old rejection forever.
|
|
181
184
|
// Controller packet replay is narrower and only resets explicit
|
|
182
185
|
// SESSION_BUSY admission races in the inbound dispatcher.
|
|
183
186
|
return code === "SESSION_BUSY" ||
|
|
187
|
+
code === "CREW_SESSION_BUSY" ||
|
|
184
188
|
code === "TURN_START_FAILED" ||
|
|
185
189
|
code === "PROMPT_ADMISSION_TIMEOUT" ||
|
|
186
190
|
code === "CREW_SESSION_STATUS_UNAVAILABLE" ||
|
|
@@ -76,14 +76,17 @@ async function refreshNativeHistory(session, config, requestId, strict, orchestr
|
|
|
76
76
|
// explicit adoption status because they were created before the
|
|
77
77
|
// native-history state machine existed. Complete that migration on
|
|
78
78
|
// the normal load/read path, preserving the existing HE thread id.
|
|
79
|
-
|
|
79
|
+
const needsManagedOwnershipRepair = metadata.nativeHandoff === "manual-sequential" && metadata.nativeHistoryChangedExternally !== true;
|
|
80
|
+
if (needsManagedOwnershipRepair || (metadata.headlessNativeAdoptionStatus !== "complete" && Array.isArray(metadata.nativeHistoryTurnIds))) {
|
|
80
81
|
await entry.client.adoptThread({
|
|
81
82
|
threadId: session.runtimeBinding.threadId,
|
|
82
83
|
providerSessionId,
|
|
83
84
|
cwd: session.cwd,
|
|
84
85
|
provider: entry.provider,
|
|
85
86
|
nativeIdentity: expectedNativeIdentity,
|
|
87
|
+
sessionContext: { role: "crew" },
|
|
86
88
|
manualHandoff: true,
|
|
89
|
+
managedHandoff: true,
|
|
87
90
|
});
|
|
88
91
|
}
|
|
89
92
|
}
|
|
@@ -1003,8 +1003,12 @@ function promptFailureCode(phase, error) {
|
|
|
1003
1003
|
return "PROMPT_DECRYPT_FAILED";
|
|
1004
1004
|
if (phase === "decrypt-memory-options" && isCryptoOperationFailure(error))
|
|
1005
1005
|
return "MEMORY_OPTIONS_DECRYPT_FAILED";
|
|
1006
|
-
if (phase === "start-turn" || phase === "attach-turn" || phase === "persist-runtime-start")
|
|
1006
|
+
if (phase === "start-turn" || phase === "attach-turn" || phase === "persist-runtime-start") {
|
|
1007
|
+
const code = structuredPromptErrorCode(error);
|
|
1008
|
+
if (code && knownPromptStartErrorCode(code))
|
|
1009
|
+
return code;
|
|
1007
1010
|
return "TURN_START_FAILED";
|
|
1011
|
+
}
|
|
1008
1012
|
if (phase === "stream-turn" || phase === "await-turn-result")
|
|
1009
1013
|
return "TURN_RUNTIME_FAILED";
|
|
1010
1014
|
if (phase === "refresh-runtime-metadata")
|
|
@@ -1013,6 +1017,20 @@ function promptFailureCode(phase, error) {
|
|
|
1013
1017
|
return "SESSION_EVENT_EMIT_FAILED";
|
|
1014
1018
|
return "PROMPT_EXECUTION_FAILED";
|
|
1015
1019
|
}
|
|
1020
|
+
function structuredPromptErrorCode(error) {
|
|
1021
|
+
if (!error || typeof error !== "object" || !("code" in error))
|
|
1022
|
+
return undefined;
|
|
1023
|
+
const code = error.code;
|
|
1024
|
+
return typeof code === "string" && code.trim() ? code : undefined;
|
|
1025
|
+
}
|
|
1026
|
+
function knownPromptStartErrorCode(code) {
|
|
1027
|
+
return code === "NATIVE_HANDOFF_RECONFIRM_REQUIRED" ||
|
|
1028
|
+
code === "NATIVE_HISTORY_CONFLICT" ||
|
|
1029
|
+
code === "CAPABILITY_UNSUPPORTED" ||
|
|
1030
|
+
code === "SESSION_BUSY" ||
|
|
1031
|
+
code === "RUNTIME_UNAVAILABLE" ||
|
|
1032
|
+
code === "REQUEST_TIMEOUT";
|
|
1033
|
+
}
|
|
1016
1034
|
function promptFailureMessage(code, error) {
|
|
1017
1035
|
if (code === "PROMPT_DECRYPT_FAILED") {
|
|
1018
1036
|
return "Failed to decrypt the prompt for this daemon. Re-pair the daemon with the correct account secret.";
|
package/apps/daemon/package.json
CHANGED
package/package.json
CHANGED
|
@@ -190,7 +190,7 @@ class ClaudeHeadlessRuntimeProvider {
|
|
|
190
190
|
if (takeoverState === "active") {
|
|
191
191
|
throw new HeadlessRuntimeCoreError("INVALID_REQUEST", `Claude native session ${providerSessionId} is active or awaiting approval; safe takeover is not supported.`);
|
|
192
192
|
}
|
|
193
|
-
if (takeoverState === "unknown" && input.manualHandoff !== true) {
|
|
193
|
+
if (takeoverState === "unknown" && input.manualHandoff !== true && input.managedHandoff !== true) {
|
|
194
194
|
throw new HeadlessRuntimeCoreError("CAPABILITY_UNSUPPORTED", `Claude native session ${providerSessionId} has no verifiable idle/takeover state in the configured SDK.`);
|
|
195
195
|
}
|
|
196
196
|
if (input.manualHandoff === true && input.manualHandoffProof?.updatedAt !== undefined && typeof info.lastModified === "number" && info.lastModified > input.manualHandoffProof.updatedAt) {
|
|
@@ -200,7 +200,7 @@ class ClaudeHeadlessRuntimeProvider {
|
|
|
200
200
|
const history = claudeNativeHistory(providerSessionId, info, messages);
|
|
201
201
|
this.sessionsByThread.set(input.thread.id, providerSessionId);
|
|
202
202
|
this.startedThreads.add(input.thread.id);
|
|
203
|
-
return { providerSessionId, history, ...(input.manualHandoff ? { providerMetadata: { nativeHandoff: "manual-sequential" } } : {}) };
|
|
203
|
+
return { providerSessionId, history, ...(input.manualHandoff && input.managedHandoff !== true ? { providerMetadata: { nativeHandoff: "manual-sequential" } } : {}) };
|
|
204
204
|
}
|
|
205
205
|
async readNativeHistory(input) {
|
|
206
206
|
if (this.transport !== "sdk" || !input.thread.providerSessionId) {
|
|
@@ -227,7 +227,7 @@ class CodexHeadlessRuntimeProvider {
|
|
|
227
227
|
if (takeoverState === "active") {
|
|
228
228
|
throw new HeadlessRuntimeCoreError("INVALID_REQUEST", `Codex native session ${input.thread.providerSessionId} is active or awaiting approval; safe takeover is not supported.`);
|
|
229
229
|
}
|
|
230
|
-
if (takeoverState === "unknown" && input.manualHandoff !== true) {
|
|
230
|
+
if (takeoverState === "unknown" && input.manualHandoff !== true && input.managedHandoff !== true) {
|
|
231
231
|
throw new HeadlessRuntimeCoreError("CAPABILITY_UNSUPPORTED", `Codex native session ${input.thread.providerSessionId} has no verifiable idle/takeover state in the configured app-server.`);
|
|
232
232
|
}
|
|
233
233
|
const nativeUpdatedAt = epochMs(nativeThread.updatedAt ?? nativeThread.updated_at ?? nativeThread.recencyAt ?? nativeThread.recency_at);
|
|
@@ -259,7 +259,7 @@ class CodexHeadlessRuntimeProvider {
|
|
|
259
259
|
const binding = codexProviderThreadBinding(input.thread.providerSessionId, input.thread.providerMetadata, input.thread.model, input.thread.thinkingLevel, codexClientProfileForThread(input.thread));
|
|
260
260
|
return {
|
|
261
261
|
...binding,
|
|
262
|
-
...(input.manualHandoff ? { providerMetadata: { ...binding.providerMetadata, nativeHandoff: "manual-sequential" } } : {}),
|
|
262
|
+
...(input.manualHandoff && input.managedHandoff !== true ? { providerMetadata: { ...binding.providerMetadata, nativeHandoff: "manual-sequential" } } : {}),
|
|
263
263
|
history: nativeHistory,
|
|
264
264
|
};
|
|
265
265
|
}
|
|
@@ -19,6 +19,7 @@ export type HeadlessRuntimeProvider = {
|
|
|
19
19
|
startThread?(input: HeadlessRuntimeProviderThreadInput): Promise<HeadlessRuntimeProviderThreadBinding | void>;
|
|
20
20
|
adoptThread?(input: HeadlessRuntimeProviderThreadInput & {
|
|
21
21
|
manualHandoff?: boolean;
|
|
22
|
+
managedHandoff?: boolean;
|
|
22
23
|
manualHandoffProof?: AdoptThreadParams["manualHandoffProof"];
|
|
23
24
|
}): Promise<HeadlessRuntimeProviderThreadBinding | void>;
|
|
24
25
|
validateNativeHandoff?(input: {
|
|
@@ -155,6 +155,12 @@ export class HeadlessRuntimeCore {
|
|
|
155
155
|
if (!this.provider.adoptThread) {
|
|
156
156
|
throw new HeadlessRuntimeCoreError("CAPABILITY_UNSUPPORTED", `Provider ${this.provider.id} does not support native session adoption.`);
|
|
157
157
|
}
|
|
158
|
+
if (params.managedHandoff === true) {
|
|
159
|
+
const existing = this.projection.getThread(params.threadId);
|
|
160
|
+
if (!existing || existing.sessionContext?.role !== "crew") {
|
|
161
|
+
throw new HeadlessRuntimeCoreError("INVALID_REQUEST", "Managed native adoption is restricted to an existing persisted Crew-owned Headless thread.");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
158
164
|
const key = nativeAdoptionOperationKey(params.threadId, params.providerSessionId);
|
|
159
165
|
const activeForThread = this.activeNativeAdoptionsByThread.get(params.threadId);
|
|
160
166
|
if (activeForThread)
|
|
@@ -200,6 +206,29 @@ export class HeadlessRuntimeCore {
|
|
|
200
206
|
return await this.adoptLegacyLoadedThread(existing, params, key);
|
|
201
207
|
}
|
|
202
208
|
assertNativeAdoptionIdentity(existing, params, this.provider.id);
|
|
209
|
+
const existingMetadata = recordFrom(existing.providerMetadata);
|
|
210
|
+
// A legacy orchestration Crew was already HE-owned before the native
|
|
211
|
+
// history migration ran. R5 could materialize that thread through the
|
|
212
|
+
// provider's manual takeover path, which left the external-handoff
|
|
213
|
+
// fence on an otherwise HE-owned Crew. Allow the daemon to durably
|
|
214
|
+
// reclassify that one managed binding without weakening native-session
|
|
215
|
+
// fencing for ordinary source:"native" threads.
|
|
216
|
+
if (params.managedHandoff === true &&
|
|
217
|
+
existingMetadata.nativeHandoff === "manual-sequential" &&
|
|
218
|
+
existingMetadata.nativeHistoryChangedExternally !== true) {
|
|
219
|
+
if (existing.sessionContext?.role !== "crew" || normalizedSessionContext(params.sessionContext)?.role !== "crew") {
|
|
220
|
+
throw new HeadlessRuntimeCoreError("INVALID_REQUEST", "Managed native adoption is restricted to an existing Crew-owned Headless thread.");
|
|
221
|
+
}
|
|
222
|
+
const nextMetadata = { ...existingMetadata };
|
|
223
|
+
delete nextMetadata.nativeHandoff;
|
|
224
|
+
delete nextMetadata.nativeHandoffProof;
|
|
225
|
+
const managedThread = { ...existing, providerMetadata: nextMetadata, updatedAt: this.now() };
|
|
226
|
+
await this.appendEvent("thread/nativeHandoffManaged", {
|
|
227
|
+
threadId: managedThread.id,
|
|
228
|
+
thread: managedThread,
|
|
229
|
+
});
|
|
230
|
+
return { thread: this.requireThread(managedThread.id) };
|
|
231
|
+
}
|
|
203
232
|
const adoptionStatus = nativeAdoptionStatus(existing.providerMetadata);
|
|
204
233
|
if (adoptionStatus === "unknown" || adoptionStatus === "reserved" || adoptionStatus === "importing") {
|
|
205
234
|
throw new HeadlessRuntimeCoreError("REQUEST_TIMEOUT", `Native session adoption for ${params.providerSessionId} is incomplete and requires reconciliation before retry.`);
|
|
@@ -278,6 +307,7 @@ export class HeadlessRuntimeCore {
|
|
|
278
307
|
thread,
|
|
279
308
|
params: params,
|
|
280
309
|
manualHandoff: params.manualHandoff === true,
|
|
310
|
+
...(params.managedHandoff ? { managedHandoff: true } : {}),
|
|
281
311
|
...(params.manualHandoffProof ? { manualHandoffProof: params.manualHandoffProof } : {}),
|
|
282
312
|
});
|
|
283
313
|
}
|
|
@@ -357,6 +387,7 @@ export class HeadlessRuntimeCore {
|
|
|
357
387
|
thread: identityReady,
|
|
358
388
|
params: params,
|
|
359
389
|
manualHandoff: true,
|
|
390
|
+
...(params.managedHandoff ? { managedHandoff: true } : {}),
|
|
360
391
|
...(params.manualHandoffProof ? { manualHandoffProof: params.manualHandoffProof } : {}),
|
|
361
392
|
});
|
|
362
393
|
}
|
|
@@ -2902,6 +2933,9 @@ function assertNativeAdoptionParamsMatch(left, right, providerId) {
|
|
|
2902
2933
|
if (leftProvider !== rightProvider || left.model !== right.model || left.thinkingLevel !== right.thinkingLevel) {
|
|
2903
2934
|
throw new HeadlessRuntimeCoreError("INVALID_REQUEST", "Native session adoption is already in flight with different provider parameters.");
|
|
2904
2935
|
}
|
|
2936
|
+
if (left.managedHandoff !== right.managedHandoff) {
|
|
2937
|
+
throw new HeadlessRuntimeCoreError("INVALID_REQUEST", "Native session adoption is already in flight with a different ownership mode.");
|
|
2938
|
+
}
|
|
2905
2939
|
const leftRole = normalizedSessionContext(left.sessionContext)?.role;
|
|
2906
2940
|
const rightRole = normalizedSessionContext(right.sessionContext)?.role;
|
|
2907
2941
|
if (leftRole !== rightRole) {
|
|
@@ -185,6 +185,7 @@ async function dispatch(core, request) {
|
|
|
185
185
|
...(typeof params.nativeIdentity === "string" && params.nativeIdentity.trim() ? { nativeIdentity: params.nativeIdentity.trim() } : {}),
|
|
186
186
|
...(isSessionContext(params.sessionContext) || params.sessionContext === null ? { sessionContext: params.sessionContext } : {}),
|
|
187
187
|
...(params.manualHandoff === true ? { manualHandoff: true } : {}),
|
|
188
|
+
...(params.managedHandoff === true ? { managedHandoff: true } : {}),
|
|
188
189
|
...(isManualHandoffProof(params.manualHandoffProof) ? { manualHandoffProof: params.manualHandoffProof } : {}),
|
|
189
190
|
});
|
|
190
191
|
case "thread/native-sync":
|
|
@@ -263,6 +263,13 @@ export type AdoptThreadParams = {
|
|
|
263
263
|
sessionContext?: ThreadSessionContext | null;
|
|
264
264
|
/** One-time user confirmation that the independent native client was stopped. */
|
|
265
265
|
manualHandoff?: boolean;
|
|
266
|
+
/**
|
|
267
|
+
* The native session was created and is owned by Headless/Happy Elves. This
|
|
268
|
+
* still uses the provider's takeover path for legacy recovery, but must not
|
|
269
|
+
* turn the thread into an externally-owned manual handoff that requires a
|
|
270
|
+
* user proof before every subsequent HE turn.
|
|
271
|
+
*/
|
|
272
|
+
managedHandoff?: boolean;
|
|
266
273
|
/** Scope and history version observed by the user before confirming handoff. */
|
|
267
274
|
manualHandoffProof?: {
|
|
268
275
|
identity: string;
|