@cydm/happy-elves 0.1.0-beta.506 → 0.1.0-beta.507
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 +2 -2
- package/apps/cli/package.json +1 -1
- package/apps/daemon/dist/orchestration/handler.js +11 -1
- package/apps/daemon/package.json +1 -1
- package/package.json +2 -2
- package/packages/agent-sdk/dist/index.d.ts +3 -2
- package/packages/agent-sdk/dist/index.js +1 -0
- package/packages/headless-runtime/dist/client.d.ts +5 -0
- package/packages/headless-runtime/dist/client.js +1 -0
- package/packages/headless-runtime/dist/core.d.ts +7 -0
- package/packages/headless-runtime/dist/core.js +13 -0
- package/packages/headless-runtime/dist/index.js +2 -0
- package/packages/headless-runtime/dist/provider-command.js +76 -0
- package/packages/pie-provider/dist/index.js +261 -5
package/README.md
CHANGED
|
@@ -6,8 +6,8 @@ Public beta CLI for Happy Elves.
|
|
|
6
6
|
|
|
7
7
|
Run once in the existing user account; it detects %USERPROFILE%\.happy-elves\hosted, keeps the current daemon and sessions running, installs the independent updater, and registers per-user startup.
|
|
8
8
|
|
|
9
|
-
npm.cmd install --prefix $HOME\.happy-elves\management-entry --ignore-scripts --no-audit --no-fund --package-lock=false @cydm/happy-elves@0.1.0-beta.
|
|
10
|
-
npm.cmd exec --yes --package @cydm/happy-elves@0.1.0-beta.
|
|
9
|
+
npm.cmd install --prefix $HOME\.happy-elves\management-entry --ignore-scripts --no-audit --no-fund --package-lock=false @cydm/happy-elves@0.1.0-beta.507
|
|
10
|
+
npm.cmd exec --yes --package @cydm/happy-elves@0.1.0-beta.507 -- happy-elves-updater install --state-root $HOME\.happy-elves\hosted --autostart
|
|
11
11
|
|
|
12
12
|
The same updater entrypoint works on macOS after selecting a permanent user prefix.
|
|
13
13
|
|
package/apps/cli/package.json
CHANGED
|
@@ -607,13 +607,23 @@ async function followUpQueueRecord(config, record) {
|
|
|
607
607
|
}
|
|
608
608
|
catch { /* encrypted content is optional in projection */ }
|
|
609
609
|
}
|
|
610
|
+
// Once the owner has delivered the prompt, prefer the runtime's canonical
|
|
611
|
+
// turn identity. The command turn is a durable admission alias used while
|
|
612
|
+
// the prompt is being sent; provider history may materialize the same turn
|
|
613
|
+
// under a different (canonical) id after a restart. Returning that binding
|
|
614
|
+
// lets a reloaded Web client reconcile the historical user item without
|
|
615
|
+
// injecting a second placeholder. Queued records still expose their
|
|
616
|
+
// reserved command turn, when one exists.
|
|
617
|
+
const turnId = record.dispatch === "delivered"
|
|
618
|
+
? record.runtimeTurnId ?? record.turnId
|
|
619
|
+
: record.turnId;
|
|
610
620
|
return {
|
|
611
621
|
id: record.followUpId ?? record.taskId,
|
|
612
622
|
requestId: record.requestId,
|
|
613
623
|
sessionId: record.consumerSessionId,
|
|
614
624
|
machineId: record.crewMachineId ?? config.machineId,
|
|
615
625
|
status: record.followUpStatus ?? (record.dispatch === "delivered" ? "delivered" : record.dispatch === "failed" ? "failed" : "queued"),
|
|
616
|
-
...(
|
|
626
|
+
...(turnId ? { turnId } : {}),
|
|
617
627
|
...(text !== undefined ? { text } : {}),
|
|
618
628
|
createdAt: record.createdAt,
|
|
619
629
|
...(record.dispatch === "delivered" ? { deliveredAt: record.updatedAt } : {}),
|
package/apps/daemon/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cydm/happy-elves",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.507",
|
|
4
4
|
"description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
|
|
5
|
-
"buildId": "
|
|
5
|
+
"buildId": "65825729e3f9e3e9a3d5a0e46b99bc22",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"happy-elves": "./apps/cli/dist/index.js",
|
|
@@ -2,7 +2,8 @@ import { type HeadlessRuntimeProvider, type HeadlessRuntimeServeOptions } from "
|
|
|
2
2
|
export type HeadlessProviderServeOptions = Omit<HeadlessRuntimeServeOptions, "provider" | "core">;
|
|
3
3
|
export declare function serveHeadlessProvider(provider: HeadlessRuntimeProvider | (() => HeadlessRuntimeProvider | Promise<HeadlessRuntimeProvider>), options?: HeadlessProviderServeOptions): Promise<void>;
|
|
4
4
|
export declare function assertHeadlessProviderConformance(provider: HeadlessRuntimeProvider): Promise<void>;
|
|
5
|
-
export type { HeadlessRuntimeProvider, HeadlessRuntimeProviderContext, HeadlessRuntimeProviderTurnInput, } from "../../headless-runtime/dist/index.js";
|
|
5
|
+
export type { HeadlessRuntimeProvider, HeadlessRuntimeProviderContext, HeadlessRuntimeProviderNativeHistory, HeadlessRuntimeProviderNativeRead, HeadlessRuntimeProviderTurnInput, } from "../../headless-runtime/dist/index.js";
|
|
6
6
|
export { addProviderTurnBinding, providerTurnBinding, rewindProviderTurnMetadata, type ProviderTurnBinding, } from "../../headless-runtime/dist/index.js";
|
|
7
|
-
export
|
|
7
|
+
export { nativeHistoryVersionForHistory, nativeHistoryVersionForTurnIds, } from "../../headless-runtime/dist/index.js";
|
|
8
|
+
export type { AgentControlParams, AgentNode, CommandExecParams, CommandExecResult, CommandExecution, CommandResizeParams, CommandTerminateParams, CommandWriteParams, ForkThreadParams, ForkThreadResult, HeadlessClientRequest, HeadlessJsonId, HeadlessNotification, HeadlessRuntimeCapabilities, HeadlessRuntimeError, HeadlessRuntimeProtocol, HeadlessServerRequest, HeadlessServerResponse, HeadlessWireMessage, HostToolDescriptor, InitializeParams, InitializeResult, InterruptTurnParams, ListAgentsParams, ListAgentsResult, ListModelsParams, ListModelsResult, ListNativeSessionsParams, ListNativeSessionsResult, NativeSessionSummary, ListThreadItemsParams, ListThreadItemsResult, ListThreadsParams, ListThreadsResult, ModelDescriptor, ListThreadTurnsParams, ListThreadTurnsResult, ReadThreadParams, ReadThreadResult, RespondServerRequestParams, RuntimeEvent, RuntimeTraceRecord, SendAgentMessageParams, ServerRequest, SpawnAgentParams, SpawnAgentResult, StartThreadParams, StartThreadResult, StartTurnParams, StartTurnResult, SteerTurnParams, Thread, ThreadItem, ThreadStatus, ThinkingLevelDescriptor, Turn, TurnError, TurnStatus, UserInput, WaitAgentParams, WaitAgentResult, WaitTurnParams, WaitTurnResult, } from "../../runtime/dist/headless.js";
|
|
8
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -10,4 +10,5 @@ export async function assertHeadlessProviderConformance(provider) {
|
|
|
10
10
|
throw new Error("provider must implement runTurn");
|
|
11
11
|
}
|
|
12
12
|
export { addProviderTurnBinding, providerTurnBinding, rewindProviderTurnMetadata, } from "../../headless-runtime/dist/index.js";
|
|
13
|
+
export { nativeHistoryVersionForHistory, nativeHistoryVersionForTurnIds, } from "../../headless-runtime/dist/index.js";
|
|
13
14
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { HeadlessRuntimeProviderNativeRead } from "./core.js";
|
|
1
2
|
import { type ForkThreadParams, type ForkThreadResult, type CancelSidechatParams, type CancelSidechatResult, type HeadlessRuntimeError, type HeadlessServerRequest, type InitializeResult, type InterruptTurnParams, type ListModelsParams, type ListModelsResult, type CommandExecParams, type CommandExecResult, type CommandResizeParams, type CommandTerminateParams, type CommandWriteParams, type ListThreadEventsParams, type ListThreadEventsResult, type ListThreadItemsParams, type ListThreadItemsResult, type ListThreadTurnsParams, type ListThreadTurnsResult, type AgentControlParams, type AdoptThreadParams, type ListNativeSessionsParams, type ListNativeSessionsResult, type ListAgentsParams, type ListAgentsResult, type ListThreadsParams, type ListThreadsResult, type ReadThreadParams, type ReadThreadResult, type RewindThreadParams, type RewindThreadResult, type RunSidechatParams, type RunSidechatResult, type ResumeThreadParams, type RespondServerRequestParams, type ServerRequestResult, type RuntimeEvent, type SendAgentMessageParams, type SpawnAgentParams, type SpawnAgentResult, type StartThreadParams, type StartThreadResult, type SyncNativeHistoryParams, type SyncNativeHistoryResult, type StartTurnParams, type StartTurnResult, type SteerTurnParams, type ThreadItem, type WaitAgentParams, type WaitAgentResult, type WaitTurnParams, type WaitTurnResult } from "../../runtime/dist/headless.js";
|
|
2
3
|
export type HeadlessRuntimeClientOptions = {
|
|
3
4
|
socketPath?: string;
|
|
@@ -15,6 +16,10 @@ export type HeadlessRuntimeClient = {
|
|
|
15
16
|
startThread(params?: StartThreadParams): Promise<StartThreadResult>;
|
|
16
17
|
adoptThread(params: AdoptThreadParams): Promise<StartThreadResult>;
|
|
17
18
|
syncNativeHistory(params: SyncNativeHistoryParams): Promise<SyncNativeHistoryResult>;
|
|
19
|
+
readNativeHistory(params: {
|
|
20
|
+
threadId: string;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
}): Promise<HeadlessRuntimeProviderNativeRead>;
|
|
18
23
|
resumeThread(params: ResumeThreadParams): Promise<StartThreadResult>;
|
|
19
24
|
forkThread(params: ForkThreadParams): Promise<ForkThreadResult>;
|
|
20
25
|
rewindThread(params: RewindThreadParams): Promise<RewindThreadResult>;
|
|
@@ -51,6 +51,7 @@ class HeadlessRuntimeClientProcess {
|
|
|
51
51
|
startThread: (params = {}) => this.request("thread/start", params),
|
|
52
52
|
adoptThread: (params) => this.request("thread/adopt", params),
|
|
53
53
|
syncNativeHistory: (params) => this.request("thread/native-sync", params),
|
|
54
|
+
readNativeHistory: (params) => this.request("thread/native-read", { threadId: params.threadId }, params.timeoutMs),
|
|
54
55
|
resumeThread: (params) => this.request("thread/resume", params),
|
|
55
56
|
forkThread: (params) => this.request("thread/fork", params),
|
|
56
57
|
rewindThread: (params) => this.request("thread/rewind", params),
|
|
@@ -210,6 +210,13 @@ export declare class HeadlessRuntimeCore {
|
|
|
210
210
|
threadId: string;
|
|
211
211
|
nativeIdentity?: string;
|
|
212
212
|
}): Promise<SyncNativeHistoryResult>;
|
|
213
|
+
/**
|
|
214
|
+
* Read provider-native history without mutating the HE projection. This is
|
|
215
|
+
* intentionally separate from native-sync so provider-command adapters can
|
|
216
|
+
* preserve the native history boundary while keeping the local runtime
|
|
217
|
+
* projection authoritative.
|
|
218
|
+
*/
|
|
219
|
+
readNativeHistory(threadId: string): Promise<HeadlessRuntimeProviderNativeRead>;
|
|
213
220
|
private syncNativeHistoryDurable;
|
|
214
221
|
resumeThread(params: ResumeThreadParams): Promise<StartThreadResult>;
|
|
215
222
|
forkThread(params: ForkThreadParams): Promise<ForkThreadResult>;
|
|
@@ -681,6 +681,19 @@ export class HeadlessRuntimeCore {
|
|
|
681
681
|
this.activeNativeHistorySyncs.delete(params.threadId);
|
|
682
682
|
}
|
|
683
683
|
}
|
|
684
|
+
/**
|
|
685
|
+
* Read provider-native history without mutating the HE projection. This is
|
|
686
|
+
* intentionally separate from native-sync so provider-command adapters can
|
|
687
|
+
* preserve the native history boundary while keeping the local runtime
|
|
688
|
+
* projection authoritative.
|
|
689
|
+
*/
|
|
690
|
+
async readNativeHistory(threadId) {
|
|
691
|
+
const thread = this.requireThread(threadId);
|
|
692
|
+
if (!thread.providerSessionId || !this.provider.readNativeHistory) {
|
|
693
|
+
throw new HeadlessRuntimeCoreError("CAPABILITY_UNSUPPORTED", `Provider ${this.provider.id} does not support read-only native history refresh.`);
|
|
694
|
+
}
|
|
695
|
+
return await this.provider.readNativeHistory({ thread, timeoutMs: this.nativeHistoryReadTimeoutMs });
|
|
696
|
+
}
|
|
684
697
|
async syncNativeHistoryDurable(params, isActive = () => true) {
|
|
685
698
|
const thread = this.requireThread(params.threadId);
|
|
686
699
|
if (!thread.providerSessionId || !this.provider.readNativeHistory) {
|
|
@@ -193,6 +193,8 @@ async function dispatch(core, request) {
|
|
|
193
193
|
threadId: requiredString(params.threadId, "threadId", request.method),
|
|
194
194
|
...(typeof params.nativeIdentity === "string" && params.nativeIdentity.trim() ? { nativeIdentity: params.nativeIdentity.trim() } : {}),
|
|
195
195
|
});
|
|
196
|
+
case "thread/native-read":
|
|
197
|
+
return await core.readNativeHistory(requiredString(params.threadId, "threadId", request.method));
|
|
196
198
|
case "thread/resume":
|
|
197
199
|
return await core.resumeThread({
|
|
198
200
|
...requiredThreadId(params, request.method),
|
|
@@ -38,6 +38,78 @@ class ProviderCommandHeadlessRuntimeProvider {
|
|
|
38
38
|
throw error;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
async listNativeSessions(input = {}) {
|
|
42
|
+
const client = await this.ensureClient();
|
|
43
|
+
try {
|
|
44
|
+
const result = await client.listNativeSessions(input);
|
|
45
|
+
return { ...result, provider: this.id };
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const code = errorCode(error);
|
|
49
|
+
if (code === "CAPABILITY_UNSUPPORTED" || code === "METHOD_NOT_FOUND") {
|
|
50
|
+
throw new HeadlessRuntimeCoreError("CAPABILITY_UNSUPPORTED", `Remote provider ${this.id} does not support native session discovery`);
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async adoptThread(input) {
|
|
56
|
+
const client = await this.ensureClient();
|
|
57
|
+
const nativeSessionId = input.thread.providerSessionId;
|
|
58
|
+
if (!nativeSessionId)
|
|
59
|
+
throw new HeadlessRuntimeCoreError("INVALID_REQUEST", "Provider command adoption requires a native provider session id.");
|
|
60
|
+
try {
|
|
61
|
+
// Keep the HE thread id stable across the command boundary. The remote
|
|
62
|
+
// runtime binds that explicit id to the provider-native session; it must
|
|
63
|
+
// not start a fresh remote thread and lose the native history.
|
|
64
|
+
const result = await client.adoptThread({
|
|
65
|
+
threadId: input.thread.id,
|
|
66
|
+
providerSessionId: nativeSessionId,
|
|
67
|
+
cwd: input.thread.cwd,
|
|
68
|
+
name: input.thread.name,
|
|
69
|
+
model: input.thread.model,
|
|
70
|
+
modelProvider: input.thread.modelProvider,
|
|
71
|
+
thinkingLevel: input.thread.thinkingLevel,
|
|
72
|
+
provider: this.id,
|
|
73
|
+
sessionContext: input.thread.sessionContext ?? null,
|
|
74
|
+
...(input.manualHandoff ? { manualHandoff: true } : {}),
|
|
75
|
+
...(input.managedHandoff ? { managedHandoff: true } : {}),
|
|
76
|
+
...(input.manualHandoffProof ? { manualHandoffProof: input.manualHandoffProof } : {}),
|
|
77
|
+
});
|
|
78
|
+
const native = await client.readNativeHistory({ threadId: result.thread.id });
|
|
79
|
+
const binding = providerCommandBindingFromThread(result.thread, {
|
|
80
|
+
remoteProviderSessionId: nativeSessionId,
|
|
81
|
+
});
|
|
82
|
+
this.remoteThreadsByThread.set(input.thread.id, binding.providerSessionId);
|
|
83
|
+
return {
|
|
84
|
+
...binding,
|
|
85
|
+
...(result.thread.providerMetadata?.nativeHistoryVersion ? { providerMetadata: { ...binding.providerMetadata, nativeHistoryVersion: result.thread.providerMetadata.nativeHistoryVersion } } : {}),
|
|
86
|
+
history: native.history,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
const code = errorCode(error);
|
|
91
|
+
if (code === "CAPABILITY_UNSUPPORTED" || code === "METHOD_NOT_FOUND") {
|
|
92
|
+
throw new HeadlessRuntimeCoreError("CAPABILITY_UNSUPPORTED", `Remote provider ${this.id} does not support native session adoption`);
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async readNativeHistory(input) {
|
|
98
|
+
const client = await this.ensureClient();
|
|
99
|
+
const remoteThreadId = remoteRuntimeThreadId(input.thread);
|
|
100
|
+
if (!remoteThreadId)
|
|
101
|
+
throw new HeadlessRuntimeCoreError("INVALID_REQUEST", "Provider command native history refresh requires a remote runtime thread id.");
|
|
102
|
+
try {
|
|
103
|
+
return await client.readNativeHistory({ threadId: remoteThreadId, timeoutMs: input.timeoutMs });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const code = errorCode(error);
|
|
107
|
+
if (code === "CAPABILITY_UNSUPPORTED" || code === "METHOD_NOT_FOUND") {
|
|
108
|
+
throw new HeadlessRuntimeCoreError("CAPABILITY_UNSUPPORTED", `Remote provider ${this.id} does not support native history refresh`);
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
41
113
|
async startThread(input) {
|
|
42
114
|
const client = await this.ensureClient();
|
|
43
115
|
const result = await client.startThread({
|
|
@@ -407,6 +479,10 @@ function providerCommandBindingFromThread(thread, extraMetadata = {}, existingMe
|
|
|
407
479
|
...(Object.keys(metadata).length > 0 ? { providerMetadata: metadata } : {}),
|
|
408
480
|
};
|
|
409
481
|
}
|
|
482
|
+
function remoteRuntimeThreadId(thread) {
|
|
483
|
+
const metadata = recordFrom(thread.providerMetadata);
|
|
484
|
+
return stringField(metadata, "remoteRuntimeThreadId") ?? (thread.providerSessionId || undefined);
|
|
485
|
+
}
|
|
410
486
|
function compactRecord(record) {
|
|
411
487
|
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
|
|
412
488
|
}
|
|
@@ -3,10 +3,10 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import readline from "node:readline";
|
|
9
|
-
import { serveHeadlessProvider, addProviderTurnBinding, providerTurnBinding, rewindProviderTurnMetadata, } from "../../agent-sdk/dist/index.js";
|
|
9
|
+
import { serveHeadlessProvider, addProviderTurnBinding, providerTurnBinding, rewindProviderTurnMetadata, nativeHistoryVersionForHistory, } from "../../agent-sdk/dist/index.js";
|
|
10
10
|
const providerId = "pie";
|
|
11
11
|
const happyElvesToolDescription = "Official Happy Elves orchestration gateway. Use when the user asks to inspect, continue, fork, rewind, wait for, or delegate work to HE sessions, machines, daemons, agents, or remote controllers. First call `skill` or `help` unless the needed command is already known from this turn. Do not guess shell commands. Do not start, stop, or reconfigure HE daemons/relays/controllers unless the user explicitly asks for HE administration.";
|
|
12
12
|
const happyElvesComputerUseToolDescription = "Official Happy Elves computer-use gateway for a trusted HE machine/session. Use for remote screen/browser/input/file-preview operations. First call `manual` or `snapshot` to understand the available surface. Do not use it for normal code edits when provider-native file/shell tools are sufficient.";
|
|
@@ -33,6 +33,90 @@ const provider = {
|
|
|
33
33
|
async listModels() {
|
|
34
34
|
return await listPieModels();
|
|
35
35
|
},
|
|
36
|
+
async listNativeSessions(input = {}) {
|
|
37
|
+
await assertPieAvailable();
|
|
38
|
+
const limit = Math.max(1, Math.min(200, Math.floor(input.limit ?? 50)));
|
|
39
|
+
const cwd = input.cwd ? canonicalPath(input.cwd) : undefined;
|
|
40
|
+
const listed = await runPieJson([
|
|
41
|
+
"sessions",
|
|
42
|
+
"list",
|
|
43
|
+
"--json",
|
|
44
|
+
"--limit",
|
|
45
|
+
String(limit),
|
|
46
|
+
...(input.cursor ? ["--cursor", input.cursor] : []),
|
|
47
|
+
...(cwd ? ["--cwd", cwd] : []),
|
|
48
|
+
]);
|
|
49
|
+
const sessions = (Array.isArray(listed.sessions) ? listed.sessions : [])
|
|
50
|
+
.map((value) => pieNativeSessionSummary(value, cwd))
|
|
51
|
+
.filter((value) => value !== undefined);
|
|
52
|
+
return {
|
|
53
|
+
provider: providerId,
|
|
54
|
+
sessions,
|
|
55
|
+
...(typeof listed.nextCursor === "string" && listed.nextCursor.length > 0 ? { nextCursor: listed.nextCursor } : {}),
|
|
56
|
+
history: "available",
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
async adoptThread(input) {
|
|
60
|
+
const providerSessionId = input.thread.providerSessionId;
|
|
61
|
+
if (!providerSessionId)
|
|
62
|
+
throw new PieProviderError("INVALID_REQUEST", "Pie native adoption requires a provider session id", true);
|
|
63
|
+
const native = await readPieNativeHistory(providerSessionId, input.thread.cwd);
|
|
64
|
+
if (native.takeoverState === "active") {
|
|
65
|
+
throw new PieProviderError("NATIVE_SESSION_ACTIVE", `Pie native session ${providerSessionId} is active and cannot be adopted`, true);
|
|
66
|
+
}
|
|
67
|
+
if (native.takeoverState === "unknown" && input.manualHandoff !== true && input.managedHandoff !== true) {
|
|
68
|
+
throw new PieProviderError("CAPABILITY_UNSUPPORTED", `Pie native session ${providerSessionId} has no verifiable idle/takeover state`, true);
|
|
69
|
+
}
|
|
70
|
+
if (input.manualHandoffProof?.updatedAt !== undefined && native.updatedAt !== undefined && native.updatedAt > input.manualHandoffProof.updatedAt) {
|
|
71
|
+
throw new PieProviderError("NATIVE_HANDOFF_RECONFIRM_REQUIRED", `Pie native session ${providerSessionId} changed after the handoff confirmation`, true);
|
|
72
|
+
}
|
|
73
|
+
const historyVersion = nativeHistoryVersionForHistory(native.history);
|
|
74
|
+
if (input.manualHandoffProof?.historyVersion && input.manualHandoffProof.historyVersion !== historyVersion) {
|
|
75
|
+
throw new PieProviderError("NATIVE_HANDOFF_RECONFIRM_REQUIRED", `Pie native session ${providerSessionId} history changed after the handoff confirmation`, true);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
providerSessionId,
|
|
79
|
+
history: native.history,
|
|
80
|
+
providerMetadata: {
|
|
81
|
+
authority: "pie-file-session-store",
|
|
82
|
+
pieSessionId: providerSessionId,
|
|
83
|
+
pieSessionInitialized: true,
|
|
84
|
+
nativeHistoryTurnIds: native.history.turns.map((turn) => turn.id),
|
|
85
|
+
nativeHistoryVersion: historyVersion,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
async readNativeHistory(input) {
|
|
90
|
+
const providerSessionId = input.thread.providerSessionId;
|
|
91
|
+
if (!providerSessionId)
|
|
92
|
+
throw new PieProviderError("INVALID_REQUEST", "Pie native history refresh requires a provider session id", true);
|
|
93
|
+
return await readPieNativeHistory(providerSessionId, input.thread.cwd, input.timeoutMs);
|
|
94
|
+
},
|
|
95
|
+
async validateNativeHandoff(input) {
|
|
96
|
+
const providerSessionId = input.thread.providerSessionId;
|
|
97
|
+
if (!providerSessionId)
|
|
98
|
+
throw new PieProviderError("INVALID_REQUEST", "Pie native handoff validation requires a provider session id", true);
|
|
99
|
+
const expectedIdentity = stringValue(recordFrom(input.thread.providerMetadata).nativeIdentity);
|
|
100
|
+
if (!expectedIdentity || input.proof?.identity !== expectedIdentity) {
|
|
101
|
+
throw new PieProviderError("NATIVE_HANDOFF_RECONFIRM_REQUIRED", "Pie native handoff proof does not match the provider session identity", true);
|
|
102
|
+
}
|
|
103
|
+
const native = await readPieNativeHistory(providerSessionId, input.thread.cwd);
|
|
104
|
+
if (native.takeoverState === "active")
|
|
105
|
+
throw new PieProviderError("NATIVE_HANDOFF_RECONFIRM_REQUIRED", `Pie native session ${providerSessionId} is still active`, true);
|
|
106
|
+
const historyVersion = nativeHistoryVersionForHistory(native.history);
|
|
107
|
+
if (!input.proof?.historyVersion || input.proof.historyVersion !== historyVersion) {
|
|
108
|
+
throw new PieProviderError("NATIVE_HANDOFF_RECONFIRM_REQUIRED", `Pie native session ${providerSessionId} changed after the handoff confirmation`, true);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
providerSessionId,
|
|
112
|
+
providerMetadata: {
|
|
113
|
+
...recordFrom(input.thread.providerMetadata),
|
|
114
|
+
nativeHistoryTurnIds: native.history.turns.map((turn) => turn.id),
|
|
115
|
+
nativeHistoryVersion: historyVersion,
|
|
116
|
+
nativeHistoryChangedExternally: undefined,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
},
|
|
36
120
|
async startThread(input) {
|
|
37
121
|
return pieThreadBinding(input, false);
|
|
38
122
|
},
|
|
@@ -508,8 +592,117 @@ async function listPieModels() {
|
|
|
508
592
|
warnings: [...warnings, ...fromConfig.warnings],
|
|
509
593
|
};
|
|
510
594
|
}
|
|
511
|
-
async function
|
|
512
|
-
|
|
595
|
+
async function readPieNativeHistory(providerSessionId, requestedCwd, timeoutMs) {
|
|
596
|
+
await assertPieAvailable();
|
|
597
|
+
const pages = [];
|
|
598
|
+
let before;
|
|
599
|
+
let session = {};
|
|
600
|
+
let pageCount = 0;
|
|
601
|
+
do {
|
|
602
|
+
if (++pageCount > 256)
|
|
603
|
+
throw new PieProviderError("PIE_HISTORY_TOO_LARGE", `Pie native session ${providerSessionId} history exceeded the supported page limit`, true);
|
|
604
|
+
const response = await runPieJson([
|
|
605
|
+
"sessions",
|
|
606
|
+
"read",
|
|
607
|
+
providerSessionId,
|
|
608
|
+
"--json",
|
|
609
|
+
"--raw",
|
|
610
|
+
"--limit",
|
|
611
|
+
"200",
|
|
612
|
+
...(before ? ["--before", before] : []),
|
|
613
|
+
], timeoutMs);
|
|
614
|
+
if (isRecord(response.session))
|
|
615
|
+
session = response.session;
|
|
616
|
+
pages.unshift(Array.isArray(response.events) ? response.events : []);
|
|
617
|
+
before = typeof response.nextBefore === "string" && response.nextBefore.length > 0 && response.hasMoreBefore === true
|
|
618
|
+
? response.nextBefore
|
|
619
|
+
: undefined;
|
|
620
|
+
} while (before);
|
|
621
|
+
const rowCwd = stringValue(session.cwd);
|
|
622
|
+
if (rowCwd && canonicalPath(rowCwd) !== canonicalPath(requestedCwd)) {
|
|
623
|
+
throw new PieProviderError("INVALID_REQUEST", `Pie native session ${providerSessionId} belongs to ${rowCwd}, not ${requestedCwd}`, true);
|
|
624
|
+
}
|
|
625
|
+
const events = pages.flat();
|
|
626
|
+
const history = pieNativeHistory(providerSessionId, events);
|
|
627
|
+
return {
|
|
628
|
+
history,
|
|
629
|
+
takeoverState: pieTakeoverState(session),
|
|
630
|
+
...(epochMs(session.updatedAt ?? session.updated_at) !== undefined ? { updatedAt: epochMs(session.updatedAt ?? session.updated_at) } : {}),
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function pieNativeHistory(providerSessionId, events) {
|
|
634
|
+
const turns = [];
|
|
635
|
+
let current;
|
|
636
|
+
let currentNativeTurnId;
|
|
637
|
+
let itemIndex = 0;
|
|
638
|
+
for (const raw of events) {
|
|
639
|
+
const event = recordFrom(raw);
|
|
640
|
+
const message = recordFrom(event.message);
|
|
641
|
+
const roleValue = stringValue(event.role) ?? stringValue(message.role);
|
|
642
|
+
const kind = roleValue === "user" ? "user" : roleValue === "assistant" ? "assistant" : undefined;
|
|
643
|
+
if (!kind)
|
|
644
|
+
continue;
|
|
645
|
+
const text = pieEventText(event, message);
|
|
646
|
+
if (!text)
|
|
647
|
+
continue;
|
|
648
|
+
const itemId = stringValue(event.entryId) ?? stringValue(event.id) ?? `pie-native:${providerSessionId}:item:${itemIndex}`;
|
|
649
|
+
const nativeTurnId = stringValue(event.turnId);
|
|
650
|
+
if (!current || (nativeTurnId ? currentNativeTurnId !== nativeTurnId : kind === "user" && current.items.length > 0)) {
|
|
651
|
+
if (current)
|
|
652
|
+
turns.push(current);
|
|
653
|
+
const startedAt = epochMs(event.timestamp ?? event.occurredAt);
|
|
654
|
+
current = {
|
|
655
|
+
id: nativeTurnId ? `pie-native:${providerSessionId}:turn:${nativeTurnId}` : `pie-native:${providerSessionId}:turn:${itemId}`,
|
|
656
|
+
status: "completed",
|
|
657
|
+
...(startedAt !== undefined ? { startedAt, completedAt: startedAt } : {}),
|
|
658
|
+
items: [],
|
|
659
|
+
};
|
|
660
|
+
currentNativeTurnId = nativeTurnId ?? undefined;
|
|
661
|
+
}
|
|
662
|
+
const createdAt = epochMs(event.timestamp ?? event.occurredAt);
|
|
663
|
+
current.items.push({ id: itemId, kind, text, ...(createdAt !== undefined ? { createdAt } : {}) });
|
|
664
|
+
current.providerCheckpointId = itemId;
|
|
665
|
+
if (createdAt !== undefined)
|
|
666
|
+
current.completedAt = Math.max(current.completedAt ?? createdAt, createdAt);
|
|
667
|
+
const status = stringValue(event.status);
|
|
668
|
+
if (status === "failed")
|
|
669
|
+
current.status = "failed";
|
|
670
|
+
else if (status === "cancelled" || status === "interrupted")
|
|
671
|
+
current.status = "interrupted";
|
|
672
|
+
else if (status === "completed")
|
|
673
|
+
current.status = "completed";
|
|
674
|
+
itemIndex += 1;
|
|
675
|
+
}
|
|
676
|
+
if (current)
|
|
677
|
+
turns.push(current);
|
|
678
|
+
return { turns };
|
|
679
|
+
}
|
|
680
|
+
function pieEventText(event, message) {
|
|
681
|
+
const direct = stringValue(event.content);
|
|
682
|
+
if (direct)
|
|
683
|
+
return direct;
|
|
684
|
+
const content = event.content ?? message.content ?? event.message;
|
|
685
|
+
if (typeof content === "string")
|
|
686
|
+
return content;
|
|
687
|
+
if (!Array.isArray(content))
|
|
688
|
+
return "";
|
|
689
|
+
return content.map((part) => {
|
|
690
|
+
if (typeof part === "string")
|
|
691
|
+
return part;
|
|
692
|
+
const record = recordFrom(part);
|
|
693
|
+
return stringValue(record.text) ?? stringValue(record.content) ?? "";
|
|
694
|
+
}).filter(Boolean).join("\n");
|
|
695
|
+
}
|
|
696
|
+
function pieTakeoverState(session) {
|
|
697
|
+
const status = stringValue(session.status)?.toLowerCase();
|
|
698
|
+
if (status && ["active", "busy", "running", "waiting"].includes(status))
|
|
699
|
+
return "active";
|
|
700
|
+
if (status && ["idle", "completed", "closed", "archived"].includes(status))
|
|
701
|
+
return "idle";
|
|
702
|
+
return "unknown";
|
|
703
|
+
}
|
|
704
|
+
async function runPieJson(args, timeoutMs) {
|
|
705
|
+
const output = await runPieText(args, timeoutMs);
|
|
513
706
|
const trimmed = output.trim();
|
|
514
707
|
if (!trimmed.startsWith("{"))
|
|
515
708
|
throw new PieProviderError("PIE_INVALID_JSON", `pie ${args.join(" ")} returned non-JSON output`, true);
|
|
@@ -520,7 +713,7 @@ async function runPieJson(args) {
|
|
|
520
713
|
}
|
|
521
714
|
return parsed;
|
|
522
715
|
}
|
|
523
|
-
function runPieText(args) {
|
|
716
|
+
function runPieText(args, timeoutMs) {
|
|
524
717
|
const child = spawnPie(args, {
|
|
525
718
|
env: providerEnv(),
|
|
526
719
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -536,8 +729,17 @@ function runPieText(args) {
|
|
|
536
729
|
stderrRing = `${stderrRing}${String(chunk)}`.slice(-stderrRingChars);
|
|
537
730
|
});
|
|
538
731
|
return new Promise((resolve, reject) => {
|
|
732
|
+
const timer = timeoutMs !== undefined && Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
733
|
+
? setTimeout(() => {
|
|
734
|
+
child.kill("SIGTERM");
|
|
735
|
+
reject(new PieProviderError("PIE_HISTORY_TIMEOUT", `Pie command timed out after ${Math.floor(timeoutMs)}ms`, true));
|
|
736
|
+
}, Math.floor(timeoutMs))
|
|
737
|
+
: undefined;
|
|
738
|
+
timer?.unref?.();
|
|
539
739
|
child.once("error", (error) => reject(new PieProviderError("PIE_SPAWN_FAILED", `Pie command could not be started: ${normalizeError(error).message}`, true)));
|
|
540
740
|
child.once("close", (code) => {
|
|
741
|
+
if (timer)
|
|
742
|
+
clearTimeout(timer);
|
|
541
743
|
const text = `${stdout}\n${stderr}`.trim();
|
|
542
744
|
if (code === 0)
|
|
543
745
|
resolve(text);
|
|
@@ -554,6 +756,60 @@ function parsePieJson(stdout) {
|
|
|
554
756
|
throw new PieProviderError("PIE_INVALID_JSON", `Pie command returned invalid JSON: ${normalizeError(error).message}`, true);
|
|
555
757
|
}
|
|
556
758
|
}
|
|
759
|
+
function pieNativeSessionSummary(value, requestedCwd) {
|
|
760
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
761
|
+
return undefined;
|
|
762
|
+
const row = value;
|
|
763
|
+
const providerSessionId = stringValue(row.id) ?? stringValue(row.sessionId);
|
|
764
|
+
if (!providerSessionId)
|
|
765
|
+
return undefined;
|
|
766
|
+
const rowCwd = stringValue(row.cwd) ?? requestedCwd ?? process.cwd();
|
|
767
|
+
const canonicalCwd = canonicalPath(rowCwd);
|
|
768
|
+
if (requestedCwd && canonicalCwd !== canonicalPath(requestedCwd))
|
|
769
|
+
return undefined;
|
|
770
|
+
const updatedAt = epochMs(row.updatedAt ?? row.updated_at ?? row.lastModified ?? row.createdAt ?? row.created_at);
|
|
771
|
+
if (updatedAt === undefined)
|
|
772
|
+
return undefined;
|
|
773
|
+
const createdAt = epochMs(row.createdAt ?? row.created_at);
|
|
774
|
+
const name = stringValue(row.name) ?? stringValue(row.title);
|
|
775
|
+
const summary = stringValue(row.summary) ?? name;
|
|
776
|
+
return {
|
|
777
|
+
provider: providerId,
|
|
778
|
+
providerSessionId,
|
|
779
|
+
cwd: canonicalCwd,
|
|
780
|
+
updatedAt,
|
|
781
|
+
...(createdAt !== undefined ? { createdAt } : {}),
|
|
782
|
+
...(row.archived === true ? { archived: true } : {}),
|
|
783
|
+
...(name ? { name } : {}),
|
|
784
|
+
...(summary ? { summary } : {}),
|
|
785
|
+
history: "available",
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
// macOS commonly exposes /var as a symlink to /private/var. Native session
|
|
789
|
+
// records are reported with the provider's canonical cwd, while HE may carry
|
|
790
|
+
// the user's spelling. Compare identities rather than raw path strings so
|
|
791
|
+
// native discovery/adoption works across those equivalent cwd aliases.
|
|
792
|
+
function canonicalPath(value) {
|
|
793
|
+
const resolved = path.resolve(value);
|
|
794
|
+
try {
|
|
795
|
+
return realpathSync.native(resolved);
|
|
796
|
+
}
|
|
797
|
+
catch {
|
|
798
|
+
return resolved;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function epochMs(value) {
|
|
802
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
803
|
+
return value < 10_000_000_000 ? value * 1000 : value;
|
|
804
|
+
if (typeof value === "string" && value.trim()) {
|
|
805
|
+
const numeric = Number(value);
|
|
806
|
+
if (Number.isFinite(numeric))
|
|
807
|
+
return epochMs(numeric);
|
|
808
|
+
const parsed = Date.parse(value);
|
|
809
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
810
|
+
}
|
|
811
|
+
return undefined;
|
|
812
|
+
}
|
|
557
813
|
function providerEnv(extra = {}) {
|
|
558
814
|
return {
|
|
559
815
|
...process.env,
|