alink-cli 0.9.0 → 0.10.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 CHANGED
@@ -1,50 +1,49 @@
1
1
  # alink-cli
2
2
 
3
- 在手机上或浏览器里远程控制你电脑上的 Claude Code、Codex、Gemini、Qwen、Kimi 等编码 agent。
3
+ 在手机或浏览器上,继续使用运行在你自己电脑上的 Claude Code、Codex、Gemini、Qwen、Kimi 等编码 agent。
4
4
 
5
- ## 快速开始
5
+ > **Local-first:** agent 在你的电脑上运行,项目文件和任务记录保存在本机。AgentLink 只负责加密连接设备,Hub 不保存你的对话。
6
6
 
7
- 1. **全局安装**
7
+ ## 三步开始
8
+
9
+ 需要 Node.js 22.16 或更高版本。
10
+
11
+ 1. 安装 CLI
8
12
 
9
13
  ```bash
10
14
  npm install -g alink-cli
11
15
  ```
12
16
 
13
- 2. **运行 CLI**
17
+ 2. 启动 AgentLink,并按提示登录
14
18
 
15
19
  ```bash
16
20
  alink-cli
17
21
  ```
18
22
 
19
- 3. **按提示登录 AgentLink 账号**
23
+ 3. 在手机或浏览器打开 [AgentLink 控制台](https://link.harmopath.com/login)
20
24
 
21
- 首次登录后,这台电脑会自动加入你的账号。之后直接运行 `alink-cli` 就能重新上线。
25
+ 登录同一个账号,你的电脑会自动出现。选择 agent 和工作目录,就可以开始任务或继续已有工作。
22
26
 
23
- 4. **打开手机或浏览器访问控制台**
27
+ 关闭浏览器或锁屏后,任务仍会在你的电脑上继续运行。
24
28
 
25
- 选择你的电脑、选择 agent、选择工作目录,然后发任务。输出会实时流回;锁屏或关掉浏览器后,任务仍会继续在电脑上运行。
29
+ ## 为什么是 local-first
26
30
 
27
- 同一台电脑只允许一个 AgentLink daemon 使用本机凭证运行;重复启动会被 CLI 拒绝,不会互相顶掉连接。
31
+ - **本地运行:** agent 和命令都在你的电脑上执行。
32
+ - **本地保存:** AgentLink 不会把你的项目复制到云端,任务记录保存在本机。
33
+ - **安全连接:** 设备之间端到端加密;Hub 只转发连接,不保存对话内容。
34
+ - **账号隔离:** 只有登录你账号的设备才能看到你的电脑。
28
35
 
29
36
  ## 常用命令
30
37
 
31
38
  ```bash
32
39
  alink-cli # 启动或管理连接
33
- alink-cli login # 登录并注册这台电脑
40
+ alink-cli login # 登录并添加这台电脑
34
41
  alink-cli status # 查看连接状态
35
42
  alink-cli machines # 查看账号下的电脑
36
43
  alink-cli logout # 退出登录
37
44
  ```
38
45
 
39
- ## 常用选项
40
-
41
- - `--dir <path>`:设置默认工作目录。
42
- - `--hub <url>`:连接到自己的 hub(自托管时使用)。
43
- - `--no-tui`:跳过交互界面,直接前台运行(适合脚本或非交互环境)。
44
-
45
- 需要 Node.js 22.16 或更高版本。
46
-
47
46
  ## 了解更多
48
47
 
49
- - 完整文档:[github.com/baichen99/agentlink](https://github.com/baichen99/agentlink)
50
- - 提交问题:[github.com/baichen99/agentlink/issues](https://github.com/baichen99/agentlink/issues)
48
+ - [完整文档](https://github.com/baichen99/agentlink)
49
+ - [提交问题](https://github.com/baichen99/agentlink/issues)
package/dist/bin.mjs CHANGED
@@ -14414,6 +14414,15 @@ Union([DispatchableClientOrchestrationCommand, Union([
14414
14414
  session: OrchestrationSession,
14415
14415
  createdAt: IsoDateTime
14416
14416
  }),
14417
+ Struct({
14418
+ type: Literal("thread.message.user.import"),
14419
+ commandId: CommandId,
14420
+ threadId: ThreadId,
14421
+ messageId: MessageId,
14422
+ turnId: TurnId,
14423
+ text: String$1,
14424
+ createdAt: IsoDateTime
14425
+ }),
14417
14426
  Struct({
14418
14427
  type: Literal("thread.message.assistant.delta"),
14419
14428
  commandId: CommandId,
@@ -35789,6 +35798,32 @@ const decideOrchestrationCommand = fn("decideOrchestrationCommand")(function* ({
35789
35798
  updatedAt: command.createdAt
35790
35799
  }
35791
35800
  };
35801
+ case "thread.message.user.import":
35802
+ yield* requireThread({
35803
+ readModel,
35804
+ command,
35805
+ threadId: command.threadId
35806
+ });
35807
+ return {
35808
+ ...yield* withEventBase({
35809
+ aggregateKind: "thread",
35810
+ aggregateId: command.threadId,
35811
+ occurredAt: command.createdAt,
35812
+ commandId: command.commandId
35813
+ }),
35814
+ type: "thread.message-sent",
35815
+ payload: {
35816
+ threadId: command.threadId,
35817
+ messageId: command.messageId,
35818
+ role: "user",
35819
+ text: command.text,
35820
+ attachments: [],
35821
+ turnId: command.turnId,
35822
+ streaming: false,
35823
+ createdAt: command.createdAt,
35824
+ updatedAt: command.createdAt
35825
+ }
35826
+ };
35792
35827
  case "thread.message.assistant.complete":
35793
35828
  yield* requireThread({
35794
35829
  readModel,
@@ -50911,6 +50946,29 @@ const normalizeDispatchCommand = (command) => gen(function* () {
50911
50946
  };
50912
50947
  });
50913
50948
  //#endregion
50949
+ //#region src/orchestration/Services/ProviderRuntimeIngestion.ts
50950
+ /**
50951
+ * ProviderRuntimeIngestionService - Provider runtime ingestion service interface.
50952
+ *
50953
+ * Owns background workers that consume provider runtime streams and emit
50954
+ * orchestration commands/events.
50955
+ *
50956
+ * @module ProviderRuntimeIngestionService
50957
+ */
50958
+ var ProviderRuntimeReconciliationError = class extends TaggedErrorClass()("ProviderRuntimeReconciliationError", {
50959
+ threadId: ThreadId,
50960
+ detail: String$1,
50961
+ cause: optional$3(Defect())
50962
+ }) {
50963
+ get message() {
50964
+ return `Failed to reconcile provider thread '${this.threadId}': ${this.detail}`;
50965
+ }
50966
+ };
50967
+ /**
50968
+ * ProviderRuntimeIngestionService - Service tag for runtime ingestion workers.
50969
+ */
50970
+ var ProviderRuntimeIngestionService = class extends Service$2()("t3/orchestration/Services/ProviderRuntimeIngestion/ProviderRuntimeIngestionService") {};
50971
+ //#endregion
50914
50972
  //#region src/observability/RpcInstrumentation.ts
50915
50973
  const RPC_SPAN_PREFIX = "ws.rpc";
50916
50974
  const DEFAULT_RPC_SPAN_ATTRIBUTES = {
@@ -52518,6 +52576,7 @@ const wsRpcServices = gen(function* () {
52518
52576
  crypto: yield* Crypto,
52519
52577
  projectionSnapshotQuery: yield* ProjectionSnapshotQuery,
52520
52578
  orchestrationEngine: yield* OrchestrationEngineService,
52579
+ providerRuntimeIngestion: yield* ProviderRuntimeIngestionService,
52521
52580
  keybindings: yield* Keybindings,
52522
52581
  externalLauncher: yield* ExternalLauncher,
52523
52582
  providerRegistry: yield* ProviderRegistry,
@@ -52538,7 +52597,7 @@ const wsRpcServices = gen(function* () {
52538
52597
  });
52539
52598
  const makeWsRpcLayer = (currentSession) => WsRpcGroup.toLayer(gen(function* () {
52540
52599
  const currentSessionId = currentSession.sessionId;
52541
- const { crypto, projectionSnapshotQuery, orchestrationEngine, keybindings, externalLauncher, providerRegistry, config, lifecycleEvents, serverSettings, startup, workspaceEntries, workspaceFileSystem, gitWorkspace, terminalManager, serverEnvironment, backgroundPolicy, serverAuth, bootstrapCredentials, sessions } = yield* wsRpcServices;
52600
+ const { crypto, projectionSnapshotQuery, orchestrationEngine, providerRuntimeIngestion, keybindings, externalLauncher, providerRegistry, config, lifecycleEvents, serverSettings, startup, workspaceEntries, workspaceFileSystem, gitWorkspace, terminalManager, serverEnvironment, backgroundPolicy, serverAuth, bootstrapCredentials, sessions } = yield* wsRpcServices;
52542
52601
  const providerMaintenanceRunner = yield* ProviderMaintenanceRunner;
52543
52602
  const rpcClientIds = yield* make$63(/* @__PURE__ */ new Set());
52544
52603
  yield* addFinalizer(() => get$3(rpcClientIds).pipe(flatMap$1((clientIds) => forEach(clientIds, (clientId) => backgroundPolicy.removeRpcClient(currentSessionId, clientId), { discard: true })), ignore$1));
@@ -52681,7 +52740,8 @@ const makeWsRpcLayer = (currentSession) => WsRpcGroup.toLayer(gen(function* () {
52681
52740
  });
52682
52741
  const dispatchNormalizedCommand = (normalizedCommand) => {
52683
52742
  const dispatchEffect = normalizedCommand.type === "thread.turn.start" && normalizedCommand.bootstrap ? dispatchBootstrapTurnStart(normalizedCommand) : orchestrationEngine.dispatch(normalizedCommand).pipe(mapError((cause) => toDispatchCommandError(cause, "Failed to dispatch orchestration command")));
52684
- return startup.enqueueCommand(dispatchEffect).pipe(mapError((cause) => toDispatchCommandError(cause, "Failed to dispatch orchestration command")));
52743
+ const guardedDispatchEffect = normalizedCommand.type === "thread.turn.start" && normalizedCommand.bootstrap === void 0 ? providerRuntimeIngestion.reconcileThread(normalizedCommand.threadId, { requireWritable: true }).pipe(mapError((cause) => toDispatchCommandError(cause, "Failed to reconcile provider thread")), andThen(dispatchEffect)) : dispatchEffect;
52744
+ return startup.enqueueCommand(guardedDispatchEffect).pipe(mapError((cause) => toDispatchCommandError(cause, "Failed to dispatch orchestration command")));
52685
52745
  };
52686
52746
  const loadServerConfig = gen(function* () {
52687
52747
  const keybindingsConfig = yield* keybindings.loadConfigState;
@@ -52782,6 +52842,10 @@ const makeWsRpcLayer = (currentSession) => WsRpcGroup.toLayer(gen(function* () {
52782
52842
  const liveBuffer = yield* unbounded$2();
52783
52843
  yield* forkScoped(liveStream.pipe(runForEach((item) => offer$1(liveBuffer, item))));
52784
52844
  const bufferedLiveStream = fromQueue(liveBuffer);
52845
+ yield* providerRuntimeIngestion.reconcileThread(input.threadId).pipe(mapError((cause) => new OrchestrationGetSnapshotError({
52846
+ message: cause.message,
52847
+ cause
52848
+ })));
52785
52849
  if (input.afterSequence !== void 0) {
52786
52850
  const afterSequence = input.afterSequence;
52787
52851
  const replayGap = (yield* orchestrationEngine.latestSequence) - afterSequence;
@@ -53106,7 +53170,11 @@ function makeAgentLinkFrameAssembler() {
53106
53170
  }
53107
53171
  //#endregion
53108
53172
  //#region src/tunnel/bridge.ts
53109
- const TUNNEL_SCOPES = [AuthOrchestrationReadScope, AuthOrchestrationOperateScope];
53173
+ const TUNNEL_SCOPES = [
53174
+ AuthOrchestrationReadScope,
53175
+ AuthOrchestrationOperateScope,
53176
+ AuthTerminalOperateScope
53177
+ ];
53110
53178
  function machineIdFromToken(token) {
53111
53179
  const parts = token.split(".");
53112
53180
  if (parts.length !== 3 || parts[0] !== "al1" || !parts[1]) return null;
@@ -54910,6 +54978,29 @@ const ProviderServiceLive = effect(ProviderService, fn("makeProviderService")(fu
54910
54978
  });
54911
54979
  const getCapabilities = (instanceId) => registry.getByInstance(instanceId).pipe(map$4((adapter) => adapter.capabilities));
54912
54980
  const getInstanceInfo = (instanceId) => registry.getInstanceInfo(instanceId);
54981
+ const readThread = fn("readThread")(function* (threadId) {
54982
+ const binding = getOrUndefined(yield* directory.getBinding(threadId));
54983
+ if (!binding) return yield* toValidationError("ProviderService.readThread", `Cannot read thread '${threadId}' because no persisted provider binding exists.`);
54984
+ const instanceId = yield* requireBindingInstanceId("ProviderService.readThread", binding);
54985
+ const adapter = yield* registry.getByInstance(instanceId);
54986
+ const cwd = readPersistedCwd(binding.runtimePayload);
54987
+ const modelSelection = readPersistedModelSelection(binding.runtimePayload);
54988
+ const hasResumeCursor = binding.resumeCursor !== null && binding.resumeCursor !== void 0;
54989
+ return yield* adapter.readThread({
54990
+ threadId,
54991
+ provider: binding.provider,
54992
+ providerInstanceId: instanceId,
54993
+ ...cwd ? { cwd } : {},
54994
+ ...modelSelection ? { modelSelection } : {},
54995
+ ...hasResumeCursor ? { resumeCursor: binding.resumeCursor } : {},
54996
+ runtimeMode: binding.runtimeMode ?? "full-access"
54997
+ });
54998
+ });
54999
+ const ensureSession = (threadId) => resolveRoutableSession({
55000
+ threadId,
55001
+ operation: "ProviderService.ensureSession",
55002
+ allowRecovery: true
55003
+ }).pipe(asVoid);
54913
55004
  const rollbackConversation = fn("rollbackConversation")(function* (rawInput) {
54914
55005
  const input = yield* decodeInputOrValidationError({
54915
55006
  operation: "ProviderService.rollbackConversation",
@@ -54980,6 +55071,8 @@ const ProviderServiceLive = effect(ProviderService, fn("makeProviderService")(fu
54980
55071
  listSessions,
54981
55072
  getCapabilities,
54982
55073
  getInstanceInfo,
55074
+ readThread,
55075
+ ensureSession,
54983
55076
  rollbackConversation,
54984
55077
  get streamEvents() {
54985
55078
  return fromPubSub(runtimeEventPubSub);
@@ -59348,8 +59441,8 @@ const makeClaudeAdapter = fn("makeClaudeAdapter")(function* (claudeSettings, opt
59348
59441
  catch: (cause) => toRequestError(threadId, "turn/interrupt", cause)
59349
59442
  });
59350
59443
  });
59351
- const readThread = fn("readThread")(function* (threadId) {
59352
- const context = yield* requireSession(threadId);
59444
+ const readThread = fn("readThread")(function* (input) {
59445
+ const context = yield* requireSession(input.threadId);
59353
59446
  return yield* snapshotThread(context);
59354
59447
  });
59355
59448
  const rollbackThread = fn("rollbackThread")(function* (threadId, numTurns) {
@@ -78828,8 +78921,14 @@ function updateSession(sessionRef, updates) {
78828
78921
  function parseThreadSnapshot(response) {
78829
78922
  return {
78830
78923
  threadId: response.thread.id,
78924
+ createdAt: response.thread.createdAt,
78925
+ updatedAt: response.thread.updatedAt,
78926
+ status: response.thread.status,
78831
78927
  turns: response.thread.turns.map((turn) => ({
78832
78928
  id: TurnId.make(turn.id),
78929
+ status: turn.status,
78930
+ ...turn.startedAt !== void 0 && turn.startedAt !== null ? { startedAt: turn.startedAt } : {},
78931
+ ...turn.completedAt !== void 0 && turn.completedAt !== null ? { completedAt: turn.completedAt } : {},
78833
78932
  items: turn.items
78834
78933
  }))
78835
78934
  };
@@ -78870,6 +78969,10 @@ const makeCodexSessionRuntime = (options) => gen(function* () {
78870
78969
  })));
78871
78970
  const clientContext = yield* layerChildProcess$1(child).pipe(build, provideService(Scope, runtimeScope));
78872
78971
  const client = yield* service(CodexAppServerClient).pipe(provide$1(clientContext));
78972
+ const initializeClient = yield* cached(gen(function* () {
78973
+ yield* client.request("initialize", buildCodexInitializeParams());
78974
+ yield* client.notify("initialized", void 0);
78975
+ }));
78873
78976
  const serverNotifications = yield* unbounded$2();
78874
78977
  const nowIso = map$4(now$1, formatIso);
78875
78978
  const randomUUIDv4 = (purpose) => crypto.randomUUIDv4.pipe(mapError((cause) => new CodexAppServerIdentifierGenerationError({
@@ -79348,8 +79451,7 @@ const makeCodexSessionRuntime = (options) => gen(function* () {
79348
79451
  }))), forkIn(runtimeScope));
79349
79452
  const start = fn("CodexSessionRuntime.start")(function* () {
79350
79453
  yield* emitSessionEvent("session/connecting", "Starting Codex App Server session.");
79351
- yield* client.request("initialize", buildCodexInitializeParams());
79352
- yield* client.notify("initialized", void 0);
79454
+ yield* initializeClient;
79353
79455
  const requestedModel = normalizeCodexModelSlug(options.model);
79354
79456
  const opened = yield* openCodexThread({
79355
79457
  client,
@@ -79443,6 +79545,7 @@ const makeCodexSessionRuntime = (options) => gen(function* () {
79443
79545
  }),
79444
79546
  readThread: gen(function* () {
79445
79547
  const providerThreadId = yield* readProviderThreadId;
79548
+ yield* initializeClient;
79446
79549
  return parseThreadSnapshot(yield* client.request("thread/read", {
79447
79550
  threadId: providerThreadId,
79448
79551
  includeTurns: true
@@ -79744,6 +79847,7 @@ function runtimeEventBase(event, canonicalThreadId) {
79744
79847
  return {
79745
79848
  eventId: event.id,
79746
79849
  provider: event.provider,
79850
+ ...event.providerInstanceId !== void 0 ? { providerInstanceId: event.providerInstanceId } : {},
79747
79851
  threadId: canonicalThreadId,
79748
79852
  createdAt: event.createdAt,
79749
79853
  ...event.turnId ? { turnId: event.turnId } : {},
@@ -80469,6 +80573,90 @@ function mapToRuntimeEvents(event, canonicalThreadId) {
80469
80573
  }
80470
80574
  return [];
80471
80575
  }
80576
+ function unixSecondsToIso(value, fallback) {
80577
+ return (/* @__PURE__ */ new Date((value ?? fallback) * 1e3)).toISOString();
80578
+ }
80579
+ function summarizeCodexUserInput(input) {
80580
+ switch (input.type) {
80581
+ case "text": return input.text;
80582
+ case "image":
80583
+ case "localImage": return "[Image input omitted]";
80584
+ case "audio":
80585
+ case "localAudio": return "[Audio input omitted]";
80586
+ case "skill": return `[Skill: ${input.name}]`;
80587
+ case "mention": return `[Mention: ${input.name}]`;
80588
+ }
80589
+ }
80590
+ /** Converts a native `thread/read` snapshot through the same item mapper as live events. */
80591
+ function mapCodexThreadSnapshot(canonicalThreadId, providerInstanceId, snapshot) {
80592
+ const makeEvent = (input) => {
80593
+ const itemId = input.item ? ProviderItemId.make(input.item.id) : void 0;
80594
+ const suffix = itemId ? `item:${itemId}:completed` : input.method.replace("/", ":");
80595
+ const createdAt = unixSecondsToIso(input.method === "turn/started" ? input.turn.startedAt : input.turn.completedAt, input.method === "turn/started" ? snapshot.createdAt : snapshot.updatedAt);
80596
+ const payload = input.method === "item/completed" && input.item ? {
80597
+ completedAtMs: Date.parse(createdAt),
80598
+ threadId: snapshot.threadId,
80599
+ turnId: input.turn.id,
80600
+ item: input.item
80601
+ } : {
80602
+ threadId: snapshot.threadId,
80603
+ turn: input.turn
80604
+ };
80605
+ return {
80606
+ id: EventId.make(`codex-snapshot:${snapshot.threadId}:turn:${input.turn.id}:${suffix}`),
80607
+ kind: "notification",
80608
+ provider: PROVIDER,
80609
+ providerInstanceId,
80610
+ threadId: canonicalThreadId,
80611
+ createdAt,
80612
+ method: input.method,
80613
+ turnId: input.turn.id,
80614
+ ...itemId ? { itemId } : {},
80615
+ payload
80616
+ };
80617
+ };
80618
+ return {
80619
+ threadId: canonicalThreadId,
80620
+ providerThreadId: snapshot.threadId,
80621
+ status: snapshot.status.type,
80622
+ activeFlags: snapshot.status.type === "active" ? snapshot.status.activeFlags : [],
80623
+ turns: snapshot.turns.map((turn) => {
80624
+ const terminal = turn.status !== "inProgress";
80625
+ const nativeEvents = [
80626
+ makeEvent({
80627
+ method: "turn/started",
80628
+ turn
80629
+ }),
80630
+ ...terminal ? turn.items.map((item) => makeEvent({
80631
+ method: "item/completed",
80632
+ turn,
80633
+ item
80634
+ })) : [],
80635
+ ...terminal ? [makeEvent({
80636
+ method: "turn/completed",
80637
+ turn
80638
+ })] : []
80639
+ ];
80640
+ const userMessages = turn.items.flatMap((item) => {
80641
+ if (item.type !== "userMessage") return [];
80642
+ const text = item.content.map(summarizeCodexUserInput).filter((part) => part.trim().length > 0).join("\n");
80643
+ return [{
80644
+ id: ProviderItemId.make(item.id),
80645
+ text: text.length > 0 ? text : "[Empty user message]"
80646
+ }];
80647
+ });
80648
+ return {
80649
+ id: turn.id,
80650
+ status: turn.status,
80651
+ ...turn.startedAt !== void 0 ? { startedAt: unixSecondsToIso(turn.startedAt, snapshot.createdAt) } : {},
80652
+ ...turn.completedAt !== void 0 ? { completedAt: unixSecondsToIso(turn.completedAt, snapshot.updatedAt) } : {},
80653
+ items: turn.items,
80654
+ userMessages,
80655
+ runtimeEvents: nativeEvents.flatMap((event) => mapToRuntimeEvents(event, canonicalThreadId))
80656
+ };
80657
+ })
80658
+ };
80659
+ }
80472
80660
  /**
80473
80661
  * Build a Codex provider adapter bound to a specific `CodexSettings` payload.
80474
80662
  *
@@ -80488,16 +80676,10 @@ const makeCodexAdapter = fn("makeCodexAdapter")(function* (codexConfig, options)
80488
80676
  const managedNativeEventLogger = options?.nativeEventLogger === void 0 ? nativeEventLogger : void 0;
80489
80677
  const runtimeEventQueue = yield* unbounded$2();
80490
80678
  const sessions = /* @__PURE__ */ new Map();
80491
- const startSession = (input) => scoped(gen(function* () {
80492
- if (input.provider !== void 0 && input.provider !== PROVIDER) return yield* new ProviderAdapterValidationError({
80493
- provider: PROVIDER,
80494
- operation: "startSession",
80495
- issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`
80496
- });
80497
- const existing = sessions.get(input.threadId);
80498
- if (existing && !existing.stopped) yield* suspend(() => stopSessionInternal(existing));
80679
+ const createRuntime = options?.makeRuntime ?? makeCodexSessionRuntime;
80680
+ const toRuntimeInput = (input) => {
80499
80681
  const serviceTier = input.modelSelection?.instanceId === boundInstanceId ? getCodexServiceTierOptionValue(input.modelSelection) : void 0;
80500
- const runtimeInput = {
80682
+ return {
80501
80683
  threadId: input.threadId,
80502
80684
  providerInstanceId: boundInstanceId,
80503
80685
  cwd: input.cwd ?? process.cwd(),
@@ -80510,10 +80692,20 @@ const makeCodexAdapter = fn("makeCodexAdapter")(function* (codexConfig, options)
80510
80692
  ...input.modelSelection?.instanceId === boundInstanceId ? { model: input.modelSelection.model } : {},
80511
80693
  ...serviceTier ? { serviceTier } : {}
80512
80694
  };
80695
+ };
80696
+ const startSession = (input) => scoped(gen(function* () {
80697
+ if (input.provider !== void 0 && input.provider !== PROVIDER) return yield* new ProviderAdapterValidationError({
80698
+ provider: PROVIDER,
80699
+ operation: "startSession",
80700
+ issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`
80701
+ });
80702
+ const existing = sessions.get(input.threadId);
80703
+ if (existing && !existing.stopped) yield* suspend(() => stopSessionInternal(existing));
80704
+ const runtimeInput = toRuntimeInput(input);
80513
80705
  const sessionScope = yield* make$76("sequential");
80514
80706
  let sessionScopeTransferred = false;
80515
80707
  yield* addFinalizer(() => sessionScopeTransferred ? void_$1 : close(sessionScope, void_$2));
80516
- const runtime = yield* (options?.makeRuntime ?? makeCodexSessionRuntime)(runtimeInput).pipe(provideService(Scope, sessionScope), provideService(ChildProcessSpawner, childProcessSpawner), provideService(Crypto, crypto), mapError((cause) => new ProviderAdapterProcessError({
80708
+ const runtime = yield* createRuntime(runtimeInput).pipe(provideService(Scope, sessionScope), provideService(ChildProcessSpawner, childProcessSpawner), provideService(Crypto, crypto), mapError((cause) => new ProviderAdapterProcessError({
80517
80709
  provider: PROVIDER,
80518
80710
  threadId: input.threadId,
80519
80711
  detail: cause.message,
@@ -80532,7 +80724,7 @@ const makeCodexAdapter = fn("makeCodexAdapter")(function* (codexConfig, options)
80532
80724
  return;
80533
80725
  }
80534
80726
  yield* offerAll(runtimeEventQueue, runtimeEvents);
80535
- })).pipe(forkChild);
80727
+ })).pipe(forkIn(sessionScope));
80536
80728
  const started = yield* runtime.start().pipe(mapError((cause) => new ProviderAdapterProcessError({
80537
80729
  provider: PROVIDER,
80538
80730
  threadId: input.threadId,
@@ -80593,20 +80785,24 @@ const makeCodexAdapter = fn("makeCodexAdapter")(function* (codexConfig, options)
80593
80785
  return session;
80594
80786
  });
80595
80787
  const interruptTurn = (threadId, turnId) => requireSession(threadId).pipe(flatMap$1((session) => session.runtime.interruptTurn(turnId)), mapError((cause) => cause._tag === "ProviderAdapterSessionNotFoundError" ? cause : mapCodexRuntimeError(threadId, "turn/interrupt", cause)));
80596
- const readThread = (threadId) => requireSession(threadId).pipe(flatMap$1((session) => session.runtime.readThread), mapError((cause) => cause._tag === "ProviderAdapterSessionNotFoundError" ? cause : mapCodexRuntimeError(threadId, "thread/read", cause)), map$4((snapshot) => ({
80597
- threadId,
80598
- turns: snapshot.turns
80599
- })));
80788
+ const readThread = (input) => {
80789
+ const active = sessions.get(input.threadId);
80790
+ return (active && !active.stopped ? active.runtime.readThread.pipe(mapError((cause) => mapCodexRuntimeError(input.threadId, "thread/read", cause))) : scoped(gen(function* () {
80791
+ return yield* (yield* createRuntime(toRuntimeInput(input)).pipe(provideService(ChildProcessSpawner, childProcessSpawner), provideService(Crypto, crypto), mapError((cause) => new ProviderAdapterProcessError({
80792
+ provider: PROVIDER,
80793
+ threadId: input.threadId,
80794
+ detail: cause.message,
80795
+ cause
80796
+ })))).readThread.pipe(mapError((cause) => mapCodexRuntimeError(input.threadId, "thread/read", cause)));
80797
+ }))).pipe(map$4((snapshot) => mapCodexThreadSnapshot(input.threadId, boundInstanceId, snapshot)));
80798
+ };
80600
80799
  const rollbackThread = (threadId, numTurns) => {
80601
80800
  if (!Number.isInteger(numTurns) || numTurns < 1) return fail(new ProviderAdapterValidationError({
80602
80801
  provider: PROVIDER,
80603
80802
  operation: "rollbackThread",
80604
80803
  issue: "numTurns must be an integer >= 1."
80605
80804
  }));
80606
- return requireSession(threadId).pipe(flatMap$1((session) => session.runtime.rollbackThread(numTurns)), mapError((cause) => cause._tag === "ProviderAdapterSessionNotFoundError" ? cause : mapCodexRuntimeError(threadId, "thread/rollback", cause)), map$4((snapshot) => ({
80607
- threadId,
80608
- turns: snapshot.turns
80609
- })));
80805
+ return requireSession(threadId).pipe(flatMap$1((session) => session.runtime.rollbackThread(numTurns)), mapError((cause) => cause._tag === "ProviderAdapterSessionNotFoundError" ? cause : mapCodexRuntimeError(threadId, "thread/rollback", cause)), map$4((snapshot) => mapCodexThreadSnapshot(threadId, boundInstanceId, snapshot)));
80610
80806
  };
80611
80807
  const respondToRequest = (threadId, requestId, decision) => requireSession(threadId).pipe(flatMap$1((session) => session.runtime.respondToRequest(requestId, decision)), mapError((cause) => cause._tag === "ProviderAdapterSessionNotFoundError" ? cause : mapCodexRuntimeError(threadId, "item/requestApproval/decision", cause)));
80612
80808
  const respondToUserInput = (threadId, requestId, answers) => requireSession(threadId).pipe(flatMap$1((session) => session.runtime.respondToUserInput(requestId, answers)), mapError((cause) => cause._tag === "ProviderAdapterSessionNotFoundError" ? cause : mapCodexRuntimeError(threadId, "item/tool/requestUserInput", cause)));
@@ -87081,10 +87277,11 @@ function makeAcpAgentAdapter(descriptor, settings, options) {
87081
87277
  detail: `${descriptor.displayName} ACP sessions do not emit structured user-input requests (request ${requestId}).`
87082
87278
  });
87083
87279
  });
87084
- const readThread = (threadId) => gen(function* () {
87280
+ const readThread = (input) => gen(function* () {
87281
+ const ctx = yield* requireSession(input.threadId);
87085
87282
  return {
87086
- threadId,
87087
- turns: (yield* requireSession(threadId)).turns
87283
+ threadId: input.threadId,
87284
+ turns: ctx.turns
87088
87285
  };
87089
87286
  });
87090
87287
  const rollbackThread = (threadId, numTurns) => gen(function* () {
@@ -87362,6 +87559,8 @@ function makeAcpAgentDriver(input) {
87362
87559
  defaultConfig: input.defaultConfig,
87363
87560
  create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => gen(function* () {
87364
87561
  const crypto = yield* Crypto;
87562
+ const fileSystem = yield* FileSystem;
87563
+ const path = yield* Path;
87365
87564
  const spawner = yield* ChildProcessSpawner;
87366
87565
  const httpClient = yield* HttpClient;
87367
87566
  const serverSettings = yield* ServerSettingsService;
@@ -87391,7 +87590,7 @@ function makeAcpAgentDriver(input) {
87391
87590
  instanceId
87392
87591
  });
87393
87592
  const textGeneration = yield* makeAcpAgentTextGeneration(descriptor, effectiveConfig, processEnv);
87394
- const checkProvider = checkAcpAgentProviderStatus(descriptor, effectiveConfig, processEnv).pipe(map$4(stampIdentity), provideService(Crypto, crypto), provideService(ChildProcessSpawner, spawner));
87593
+ const checkProvider = checkAcpAgentProviderStatus(descriptor, effectiveConfig, processEnv).pipe(map$4(stampIdentity), provideService(Crypto, crypto), provideService(FileSystem, fileSystem), provideService(Path, path), provideService(ChildProcessSpawner, spawner));
87395
87594
  const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
87396
87595
  const snapshot = yield* makeManagedServerProvider({
87397
87596
  maintenanceCapabilities,
@@ -87980,20 +88179,6 @@ const ProviderInstanceRegistryHydrationLive = unwrap(gen(function* () {
87980
88179
  */
87981
88180
  var ProviderCommandReactor = class extends Service$2()("t3/orchestration/Services/ProviderCommandReactor") {};
87982
88181
  //#endregion
87983
- //#region src/orchestration/Services/ProviderRuntimeIngestion.ts
87984
- /**
87985
- * ProviderRuntimeIngestionService - Provider runtime ingestion service interface.
87986
- *
87987
- * Owns background workers that consume provider runtime streams and emit
87988
- * orchestration commands/events.
87989
- *
87990
- * @module ProviderRuntimeIngestionService
87991
- */
87992
- /**
87993
- * ProviderRuntimeIngestionService - Service tag for runtime ingestion workers.
87994
- */
87995
- var ProviderRuntimeIngestionService = class extends Service$2()("t3/orchestration/Services/ProviderRuntimeIngestion/ProviderRuntimeIngestionService") {};
87996
- //#endregion
87997
88182
  //#region src/orchestration/Services/ThreadDeletionReactor.ts
87998
88183
  /**
87999
88184
  * ThreadDeletionReactor - Thread deletion cleanup reactor service interface.
@@ -88554,7 +88739,18 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
88554
88739
  const providerService = yield* ProviderService;
88555
88740
  const projectionTurnRepository = yield* ProjectionTurnRepository;
88556
88741
  const serverSettingsService = yield* ServerSettingsService;
88557
- const providerCommandId = (event, tag) => crypto.randomUUIDv4.pipe(map$4((uuid) => CommandId.make(`provider:${event.eventId}:${tag}:${uuid}`)));
88742
+ const providerCommandId = (event, tag) => String(event.eventId).startsWith("codex-snapshot:") ? succeed$1(CommandId.make(`provider:${event.eventId}:${tag}`)) : crypto.randomUUIDv4.pipe(map$4((uuid) => CommandId.make(`provider:${event.eventId}:${tag}:${uuid}`)));
88743
+ const threadLocksRef = yield* make$53(/* @__PURE__ */ new Map());
88744
+ const getThreadSemaphore = (threadId) => modifyEffect(threadLocksRef, (current) => {
88745
+ const existing = current.get(threadId);
88746
+ if (existing) return succeed$1([existing, current]);
88747
+ return make$75(1).pipe(map$4((semaphore) => {
88748
+ const next = new Map(current);
88749
+ next.set(threadId, semaphore);
88750
+ return [semaphore, next];
88751
+ }));
88752
+ });
88753
+ const withThreadLock = (threadId, effect) => flatMap$1(getThreadSemaphore(threadId), (semaphore) => semaphore.withPermit(effect));
88558
88754
  const turnMessageIdsByTurnKey = yield* make$81({
88559
88755
  capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY,
88560
88756
  timeToLive: TURN_MESSAGE_IDS_BY_TURN_TTL,
@@ -88673,7 +88869,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
88673
88869
  if (!hasRenderableAssistantText(bufferedText)) return false;
88674
88870
  yield* orchestrationEngine.dispatch({
88675
88871
  type: "thread.message.assistant.delta",
88676
- commandId: yield* providerCommandId(input.event, input.commandTag),
88872
+ commandId: yield* providerCommandId(input.event, `${input.commandTag}:${input.messageId}`),
88677
88873
  threadId: input.threadId,
88678
88874
  messageId: input.messageId,
88679
88875
  delta: bufferedText,
@@ -88701,7 +88897,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
88701
88897
  const hasRenderableText = hasRenderableAssistantText(text);
88702
88898
  if (hasRenderableText) yield* orchestrationEngine.dispatch({
88703
88899
  type: "thread.message.assistant.delta",
88704
- commandId: yield* providerCommandId(input.event, input.finalDeltaCommandTag),
88900
+ commandId: yield* providerCommandId(input.event, `${input.finalDeltaCommandTag}:${input.messageId}`),
88705
88901
  threadId: input.threadId,
88706
88902
  messageId: input.messageId,
88707
88903
  delta: text,
@@ -88710,7 +88906,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
88710
88906
  });
88711
88907
  if (input.hasProjectedMessage || hasRenderableText) yield* orchestrationEngine.dispatch({
88712
88908
  type: "thread.message.assistant.complete",
88713
- commandId: yield* providerCommandId(input.event, input.commandTag),
88909
+ commandId: yield* providerCommandId(input.event, `${input.commandTag}:${input.messageId}`),
88714
88910
  threadId: input.threadId,
88715
88911
  messageId: input.messageId,
88716
88912
  ...input.turnId ? { turnId: input.turnId } : {},
@@ -88744,7 +88940,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
88744
88940
  const existingPlan = findProposedPlanById(input.threadProposedPlans, input.planId);
88745
88941
  yield* orchestrationEngine.dispatch({
88746
88942
  type: "thread.proposed-plan.upsert",
88747
- commandId: yield* providerCommandId(input.event, "proposed-plan-upsert"),
88943
+ commandId: yield* providerCommandId(input.event, `proposed-plan-upsert:${input.planId}`),
88748
88944
  threadId: input.threadId,
88749
88945
  proposedPlan: {
88750
88946
  id: input.planId,
@@ -89088,7 +89284,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
89088
89284
  taskTitle = yield* lookupTaskDescription(thread.id, event.payload.taskId);
89089
89285
  if (!taskTitle) taskTitle = findTaskTitleInActivities((yield* getLoadedThreadDetail())?.activities, event.payload.taskId);
89090
89286
  }
89091
- yield* forEach(runtimeEventToActivities(event, taskTitle), (activity) => providerCommandId(event, "thread-activity-append").pipe(flatMap$1((commandId) => orchestrationEngine.dispatch({
89287
+ yield* forEach(runtimeEventToActivities(event, taskTitle), (activity) => providerCommandId(event, `thread-activity-append:${activity.id}`).pipe(flatMap$1((commandId) => orchestrationEngine.dispatch({
89092
89288
  type: "thread.activity.append",
89093
89289
  commandId,
89094
89290
  threadId: thread.id,
@@ -89096,6 +89292,51 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
89096
89292
  createdAt: activity.createdAt
89097
89293
  })))).pipe(asVoid);
89098
89294
  });
89295
+ const reconcileThreadUnlocked = fn("reconcileThreadUnlocked")(function* (threadId, options) {
89296
+ const thread = yield* resolveThreadShell(threadId);
89297
+ if (!thread || thread.session?.providerName !== ProviderDriverKind.make("codex")) return;
89298
+ const snapshot = yield* providerService.readThread(threadId);
89299
+ for (const turn of snapshot.turns) {
89300
+ const existing = getOrUndefined(yield* projectionTurnRepository.getByTurnId({
89301
+ threadId,
89302
+ turnId: turn.id
89303
+ }));
89304
+ const isTerminal = turn.status === "completed" || turn.status === "failed" || turn.status === "interrupted";
89305
+ if (isTerminal && existing && (existing.state === "completed" || existing.state === "error" || existing.state === "interrupted")) continue;
89306
+ if (isTerminal) {
89307
+ const pendingTurnStart = yield* projectionTurnRepository.getPendingTurnStartByThreadId({ threadId });
89308
+ if (!(existing?.pendingMessageId !== null && existing?.pendingMessageId !== void 0 ? true : isSome(pendingTurnStart))) {
89309
+ const createdAt = turn.startedAt ?? turn.runtimeEvents?.[0]?.createdAt ?? thread.session.updatedAt;
89310
+ const providerThreadId = snapshot.providerThreadId ?? String(threadId);
89311
+ for (const userMessage of turn.userMessages ?? []) yield* orchestrationEngine.dispatch({
89312
+ type: "thread.message.user.import",
89313
+ commandId: CommandId.make(`provider-user:${providerThreadId}:${turn.id}:${userMessage.id}:import`),
89314
+ threadId,
89315
+ messageId: MessageId.make(`provider-user:${providerThreadId}:${turn.id}:${userMessage.id}`),
89316
+ turnId: turn.id,
89317
+ text: userMessage.text,
89318
+ createdAt
89319
+ });
89320
+ }
89321
+ }
89322
+ const runtimeEvents = turn.runtimeEvents ?? [];
89323
+ yield* forEach(isTerminal ? runtimeEvents : runtimeEvents.filter((event) => event.type === "turn.started"), processRuntimeEvent, {
89324
+ concurrency: 1,
89325
+ discard: true
89326
+ });
89327
+ }
89328
+ if (options?.requireWritable !== true) return;
89329
+ if (snapshot.status !== "idle" && snapshot.status !== "notLoaded" || snapshot.turns.some((turn) => turn.status === "inProgress")) return yield* new ProviderRuntimeReconciliationError({
89330
+ threadId,
89331
+ detail: snapshot.status === "active" ? "Codex is still processing this thread; wait for it to finish before sending." : `Codex thread is not writable while its native status is '${snapshot.status ?? "unknown"}'.`
89332
+ });
89333
+ yield* providerService.ensureSession(threadId);
89334
+ });
89335
+ const reconcileThread = (threadId, options) => withThreadLock(threadId, reconcileThreadUnlocked(threadId, options)).pipe(mapError((cause) => cause instanceof ProviderRuntimeReconciliationError ? cause : new ProviderRuntimeReconciliationError({
89336
+ threadId,
89337
+ detail: cause instanceof Error ? cause.message : "Provider snapshot sync failed.",
89338
+ cause
89339
+ })));
89099
89340
  const processDomainEvent = (_event) => void_$1;
89100
89341
  const processInput = (input) => input.source === "runtime" ? processRuntimeEvent(input.event) : processDomainEvent(input.event);
89101
89342
  const processInputSafely = (input) => processInput(input).pipe(catchCause((cause) => {
@@ -89123,6 +89364,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
89123
89364
  });
89124
89365
  return {
89125
89366
  start,
89367
+ reconcileThread,
89126
89368
  drain: worker.drain
89127
89369
  };
89128
89370
  })).pipe(provide(ProjectionTurnRepositoryLive));