@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 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.506
10
- npm.cmd exec --yes --package @cydm/happy-elves@0.1.0-beta.506 -- happy-elves-updater install --state-root $HOME\.happy-elves\hosted --autostart
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
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-cli",
3
- "version": "0.1.0-beta.506",
3
+ "version": "0.1.0-beta.507",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -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
- ...(record.turnId ? { turnId: record.turnId } : {}),
626
+ ...(turnId ? { turnId } : {}),
617
627
  ...(text !== undefined ? { text } : {}),
618
628
  createdAt: record.createdAt,
619
629
  ...(record.dispatch === "delivered" ? { deliveredAt: record.updatedAt } : {}),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.506",
3
+ "version": "0.1.0-beta.507",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.506",
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": "80a6a4f86152f7e235be3216093c3c6b",
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 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, 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";
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 runPieJson(args) {
512
- const output = await runPieText(args);
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,