alink-cli 0.9.1 → 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;
@@ -54914,6 +54978,29 @@ const ProviderServiceLive = effect(ProviderService, fn("makeProviderService")(fu
54914
54978
  });
54915
54979
  const getCapabilities = (instanceId) => registry.getByInstance(instanceId).pipe(map$4((adapter) => adapter.capabilities));
54916
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);
54917
55004
  const rollbackConversation = fn("rollbackConversation")(function* (rawInput) {
54918
55005
  const input = yield* decodeInputOrValidationError({
54919
55006
  operation: "ProviderService.rollbackConversation",
@@ -54984,6 +55071,8 @@ const ProviderServiceLive = effect(ProviderService, fn("makeProviderService")(fu
54984
55071
  listSessions,
54985
55072
  getCapabilities,
54986
55073
  getInstanceInfo,
55074
+ readThread,
55075
+ ensureSession,
54987
55076
  rollbackConversation,
54988
55077
  get streamEvents() {
54989
55078
  return fromPubSub(runtimeEventPubSub);
@@ -59352,8 +59441,8 @@ const makeClaudeAdapter = fn("makeClaudeAdapter")(function* (claudeSettings, opt
59352
59441
  catch: (cause) => toRequestError(threadId, "turn/interrupt", cause)
59353
59442
  });
59354
59443
  });
59355
- const readThread = fn("readThread")(function* (threadId) {
59356
- const context = yield* requireSession(threadId);
59444
+ const readThread = fn("readThread")(function* (input) {
59445
+ const context = yield* requireSession(input.threadId);
59357
59446
  return yield* snapshotThread(context);
59358
59447
  });
59359
59448
  const rollbackThread = fn("rollbackThread")(function* (threadId, numTurns) {
@@ -78832,8 +78921,14 @@ function updateSession(sessionRef, updates) {
78832
78921
  function parseThreadSnapshot(response) {
78833
78922
  return {
78834
78923
  threadId: response.thread.id,
78924
+ createdAt: response.thread.createdAt,
78925
+ updatedAt: response.thread.updatedAt,
78926
+ status: response.thread.status,
78835
78927
  turns: response.thread.turns.map((turn) => ({
78836
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 } : {},
78837
78932
  items: turn.items
78838
78933
  }))
78839
78934
  };
@@ -78874,6 +78969,10 @@ const makeCodexSessionRuntime = (options) => gen(function* () {
78874
78969
  })));
78875
78970
  const clientContext = yield* layerChildProcess$1(child).pipe(build, provideService(Scope, runtimeScope));
78876
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
+ }));
78877
78976
  const serverNotifications = yield* unbounded$2();
78878
78977
  const nowIso = map$4(now$1, formatIso);
78879
78978
  const randomUUIDv4 = (purpose) => crypto.randomUUIDv4.pipe(mapError((cause) => new CodexAppServerIdentifierGenerationError({
@@ -79352,8 +79451,7 @@ const makeCodexSessionRuntime = (options) => gen(function* () {
79352
79451
  }))), forkIn(runtimeScope));
79353
79452
  const start = fn("CodexSessionRuntime.start")(function* () {
79354
79453
  yield* emitSessionEvent("session/connecting", "Starting Codex App Server session.");
79355
- yield* client.request("initialize", buildCodexInitializeParams());
79356
- yield* client.notify("initialized", void 0);
79454
+ yield* initializeClient;
79357
79455
  const requestedModel = normalizeCodexModelSlug(options.model);
79358
79456
  const opened = yield* openCodexThread({
79359
79457
  client,
@@ -79447,6 +79545,7 @@ const makeCodexSessionRuntime = (options) => gen(function* () {
79447
79545
  }),
79448
79546
  readThread: gen(function* () {
79449
79547
  const providerThreadId = yield* readProviderThreadId;
79548
+ yield* initializeClient;
79450
79549
  return parseThreadSnapshot(yield* client.request("thread/read", {
79451
79550
  threadId: providerThreadId,
79452
79551
  includeTurns: true
@@ -79748,6 +79847,7 @@ function runtimeEventBase(event, canonicalThreadId) {
79748
79847
  return {
79749
79848
  eventId: event.id,
79750
79849
  provider: event.provider,
79850
+ ...event.providerInstanceId !== void 0 ? { providerInstanceId: event.providerInstanceId } : {},
79751
79851
  threadId: canonicalThreadId,
79752
79852
  createdAt: event.createdAt,
79753
79853
  ...event.turnId ? { turnId: event.turnId } : {},
@@ -80473,6 +80573,90 @@ function mapToRuntimeEvents(event, canonicalThreadId) {
80473
80573
  }
80474
80574
  return [];
80475
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
+ }
80476
80660
  /**
80477
80661
  * Build a Codex provider adapter bound to a specific `CodexSettings` payload.
80478
80662
  *
@@ -80492,16 +80676,10 @@ const makeCodexAdapter = fn("makeCodexAdapter")(function* (codexConfig, options)
80492
80676
  const managedNativeEventLogger = options?.nativeEventLogger === void 0 ? nativeEventLogger : void 0;
80493
80677
  const runtimeEventQueue = yield* unbounded$2();
80494
80678
  const sessions = /* @__PURE__ */ new Map();
80495
- const startSession = (input) => scoped(gen(function* () {
80496
- if (input.provider !== void 0 && input.provider !== PROVIDER) return yield* new ProviderAdapterValidationError({
80497
- provider: PROVIDER,
80498
- operation: "startSession",
80499
- issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`
80500
- });
80501
- const existing = sessions.get(input.threadId);
80502
- if (existing && !existing.stopped) yield* suspend(() => stopSessionInternal(existing));
80679
+ const createRuntime = options?.makeRuntime ?? makeCodexSessionRuntime;
80680
+ const toRuntimeInput = (input) => {
80503
80681
  const serviceTier = input.modelSelection?.instanceId === boundInstanceId ? getCodexServiceTierOptionValue(input.modelSelection) : void 0;
80504
- const runtimeInput = {
80682
+ return {
80505
80683
  threadId: input.threadId,
80506
80684
  providerInstanceId: boundInstanceId,
80507
80685
  cwd: input.cwd ?? process.cwd(),
@@ -80514,10 +80692,20 @@ const makeCodexAdapter = fn("makeCodexAdapter")(function* (codexConfig, options)
80514
80692
  ...input.modelSelection?.instanceId === boundInstanceId ? { model: input.modelSelection.model } : {},
80515
80693
  ...serviceTier ? { serviceTier } : {}
80516
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);
80517
80705
  const sessionScope = yield* make$76("sequential");
80518
80706
  let sessionScopeTransferred = false;
80519
80707
  yield* addFinalizer(() => sessionScopeTransferred ? void_$1 : close(sessionScope, void_$2));
80520
- 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({
80521
80709
  provider: PROVIDER,
80522
80710
  threadId: input.threadId,
80523
80711
  detail: cause.message,
@@ -80536,7 +80724,7 @@ const makeCodexAdapter = fn("makeCodexAdapter")(function* (codexConfig, options)
80536
80724
  return;
80537
80725
  }
80538
80726
  yield* offerAll(runtimeEventQueue, runtimeEvents);
80539
- })).pipe(forkChild);
80727
+ })).pipe(forkIn(sessionScope));
80540
80728
  const started = yield* runtime.start().pipe(mapError((cause) => new ProviderAdapterProcessError({
80541
80729
  provider: PROVIDER,
80542
80730
  threadId: input.threadId,
@@ -80597,20 +80785,24 @@ const makeCodexAdapter = fn("makeCodexAdapter")(function* (codexConfig, options)
80597
80785
  return session;
80598
80786
  });
80599
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)));
80600
- 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) => ({
80601
- threadId,
80602
- turns: snapshot.turns
80603
- })));
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
+ };
80604
80799
  const rollbackThread = (threadId, numTurns) => {
80605
80800
  if (!Number.isInteger(numTurns) || numTurns < 1) return fail(new ProviderAdapterValidationError({
80606
80801
  provider: PROVIDER,
80607
80802
  operation: "rollbackThread",
80608
80803
  issue: "numTurns must be an integer >= 1."
80609
80804
  }));
80610
- 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) => ({
80611
- threadId,
80612
- turns: snapshot.turns
80613
- })));
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)));
80614
80806
  };
80615
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)));
80616
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)));
@@ -87085,10 +87277,11 @@ function makeAcpAgentAdapter(descriptor, settings, options) {
87085
87277
  detail: `${descriptor.displayName} ACP sessions do not emit structured user-input requests (request ${requestId}).`
87086
87278
  });
87087
87279
  });
87088
- const readThread = (threadId) => gen(function* () {
87280
+ const readThread = (input) => gen(function* () {
87281
+ const ctx = yield* requireSession(input.threadId);
87089
87282
  return {
87090
- threadId,
87091
- turns: (yield* requireSession(threadId)).turns
87283
+ threadId: input.threadId,
87284
+ turns: ctx.turns
87092
87285
  };
87093
87286
  });
87094
87287
  const rollbackThread = (threadId, numTurns) => gen(function* () {
@@ -87986,20 +88179,6 @@ const ProviderInstanceRegistryHydrationLive = unwrap(gen(function* () {
87986
88179
  */
87987
88180
  var ProviderCommandReactor = class extends Service$2()("t3/orchestration/Services/ProviderCommandReactor") {};
87988
88181
  //#endregion
87989
- //#region src/orchestration/Services/ProviderRuntimeIngestion.ts
87990
- /**
87991
- * ProviderRuntimeIngestionService - Provider runtime ingestion service interface.
87992
- *
87993
- * Owns background workers that consume provider runtime streams and emit
87994
- * orchestration commands/events.
87995
- *
87996
- * @module ProviderRuntimeIngestionService
87997
- */
87998
- /**
87999
- * ProviderRuntimeIngestionService - Service tag for runtime ingestion workers.
88000
- */
88001
- var ProviderRuntimeIngestionService = class extends Service$2()("t3/orchestration/Services/ProviderRuntimeIngestion/ProviderRuntimeIngestionService") {};
88002
- //#endregion
88003
88182
  //#region src/orchestration/Services/ThreadDeletionReactor.ts
88004
88183
  /**
88005
88184
  * ThreadDeletionReactor - Thread deletion cleanup reactor service interface.
@@ -88560,7 +88739,18 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
88560
88739
  const providerService = yield* ProviderService;
88561
88740
  const projectionTurnRepository = yield* ProjectionTurnRepository;
88562
88741
  const serverSettingsService = yield* ServerSettingsService;
88563
- 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));
88564
88754
  const turnMessageIdsByTurnKey = yield* make$81({
88565
88755
  capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY,
88566
88756
  timeToLive: TURN_MESSAGE_IDS_BY_TURN_TTL,
@@ -88679,7 +88869,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
88679
88869
  if (!hasRenderableAssistantText(bufferedText)) return false;
88680
88870
  yield* orchestrationEngine.dispatch({
88681
88871
  type: "thread.message.assistant.delta",
88682
- commandId: yield* providerCommandId(input.event, input.commandTag),
88872
+ commandId: yield* providerCommandId(input.event, `${input.commandTag}:${input.messageId}`),
88683
88873
  threadId: input.threadId,
88684
88874
  messageId: input.messageId,
88685
88875
  delta: bufferedText,
@@ -88707,7 +88897,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
88707
88897
  const hasRenderableText = hasRenderableAssistantText(text);
88708
88898
  if (hasRenderableText) yield* orchestrationEngine.dispatch({
88709
88899
  type: "thread.message.assistant.delta",
88710
- commandId: yield* providerCommandId(input.event, input.finalDeltaCommandTag),
88900
+ commandId: yield* providerCommandId(input.event, `${input.finalDeltaCommandTag}:${input.messageId}`),
88711
88901
  threadId: input.threadId,
88712
88902
  messageId: input.messageId,
88713
88903
  delta: text,
@@ -88716,7 +88906,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
88716
88906
  });
88717
88907
  if (input.hasProjectedMessage || hasRenderableText) yield* orchestrationEngine.dispatch({
88718
88908
  type: "thread.message.assistant.complete",
88719
- commandId: yield* providerCommandId(input.event, input.commandTag),
88909
+ commandId: yield* providerCommandId(input.event, `${input.commandTag}:${input.messageId}`),
88720
88910
  threadId: input.threadId,
88721
88911
  messageId: input.messageId,
88722
88912
  ...input.turnId ? { turnId: input.turnId } : {},
@@ -88750,7 +88940,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
88750
88940
  const existingPlan = findProposedPlanById(input.threadProposedPlans, input.planId);
88751
88941
  yield* orchestrationEngine.dispatch({
88752
88942
  type: "thread.proposed-plan.upsert",
88753
- commandId: yield* providerCommandId(input.event, "proposed-plan-upsert"),
88943
+ commandId: yield* providerCommandId(input.event, `proposed-plan-upsert:${input.planId}`),
88754
88944
  threadId: input.threadId,
88755
88945
  proposedPlan: {
88756
88946
  id: input.planId,
@@ -89094,7 +89284,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
89094
89284
  taskTitle = yield* lookupTaskDescription(thread.id, event.payload.taskId);
89095
89285
  if (!taskTitle) taskTitle = findTaskTitleInActivities((yield* getLoadedThreadDetail())?.activities, event.payload.taskId);
89096
89286
  }
89097
- 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({
89098
89288
  type: "thread.activity.append",
89099
89289
  commandId,
89100
89290
  threadId: thread.id,
@@ -89102,6 +89292,51 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
89102
89292
  createdAt: activity.createdAt
89103
89293
  })))).pipe(asVoid);
89104
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
+ })));
89105
89340
  const processDomainEvent = (_event) => void_$1;
89106
89341
  const processInput = (input) => input.source === "runtime" ? processRuntimeEvent(input.event) : processDomainEvent(input.event);
89107
89342
  const processInputSafely = (input) => processInput(input).pipe(catchCause((cause) => {
@@ -89129,6 +89364,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
89129
89364
  });
89130
89365
  return {
89131
89366
  start,
89367
+ reconcileThread,
89132
89368
  drain: worker.drain
89133
89369
  };
89134
89370
  })).pipe(provide(ProjectionTurnRepositoryLive));