@tomflow/proflow-platform-host 0.1.21 → 0.1.23

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/dist/src/index.js CHANGED
@@ -4,14 +4,15 @@ import { appendFile, chmod, mkdir, readFile, stat, writeFile, } from "node:fs/pr
4
4
  import { createServer } from "node:http";
5
5
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
6
6
  import { createAgentRuntime } from "@tomflow/proflow-agent-runtime";
7
- import { executionCapabilityIds, } from "@tomflow/proflow-execution-contracts";
7
+ import { createLocalToolBridgeHostClient, LocalToolBridgeError, } from "@tomflow/proflow-execution-browser-extension/local-tool-bridge";
8
8
  import { applyMigrations } from "@tomflow/proflow-task-migration-runner";
9
9
  import { createTaskServices, publicOperationNames, } from "@tomflow/proflow-task-orchestration";
10
10
  import { SqliteTaskStore } from "@tomflow/proflow-task-store-sqlite";
11
11
  import { taskMigrations } from "@tomflow/proflow-task-store-sqlite/migrations";
12
12
  import { z } from "zod";
13
13
  import { classifyBrowserPermission, } from "./browser-permission-policy.js";
14
- import { roleOperations, rolePackageRefs, } from "./role-operations.js";
14
+ import { createReconciliationCoordinator } from "./reconciliation-coordinator.js";
15
+ import { directToolActionIds, roleAllowsDirectToolOperation, roleOperations, rolePackageRefs, } from "./role-operations.js";
15
16
  const loopbackHosts = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
16
17
  // Business owner calls are Promise-driven. This is only a last-resort transport
17
18
  // watchdog for a hung local call, not a normal workflow deadline.
@@ -67,16 +68,6 @@ const browserStructuredLogSchema = z
67
68
  tabId: z.number().int().nonnegative().optional(),
68
69
  })
69
70
  .strict();
70
- const taskDiagnosticReasonResultSchema = z
71
- .object({
72
- finding: z.string().min(1).max(1_000),
73
- probableCause: z.string().min(1).max(1_000),
74
- confidence: z.number().min(0).max(1),
75
- recommendedNextObservation: z.string().min(1).max(1_000),
76
- recommendedRecoveryAction: z.string().min(1).max(1_000),
77
- needsHumanAttention: z.boolean(),
78
- })
79
- .strict();
80
71
  const taskDocumentFileSchema = z.object({
81
72
  taskId: z.string().min(1),
82
73
  documentType: z.string().min(1),
@@ -102,6 +93,40 @@ function fileBridgeOutputForTaskResult(result) {
102
93
  };
103
94
  return output;
104
95
  }
96
+ const directToolPlatformIdentityFields = new Set([
97
+ "actorRef",
98
+ "authenticatedRoleRef",
99
+ "callerRef",
100
+ "roleRef",
101
+ "taskId",
102
+ "nodeId",
103
+ "runNo",
104
+ "workerRef",
105
+ "executionRef",
106
+ "workspaceRoot",
107
+ "deadlineAt",
108
+ "commandId",
109
+ "commandDigest",
110
+ "generation",
111
+ "extensionId",
112
+ "extensionInstanceId",
113
+ ]);
114
+ function assertNoDirectToolPlatformIdentity(value) {
115
+ if (Array.isArray(value)) {
116
+ for (const item of value)
117
+ assertNoDirectToolPlatformIdentity(item);
118
+ return;
119
+ }
120
+ if (typeof value !== "object" || value === null)
121
+ return;
122
+ for (const [key, item] of Object.entries(value)) {
123
+ if (directToolPlatformIdentityFields.has(key))
124
+ throw Object.assign(new Error("DIRECT_TOOL_IDENTITY_FIELD_DENIED"), {
125
+ httpStatus: 400,
126
+ });
127
+ assertNoDirectToolPlatformIdentity(item);
128
+ }
129
+ }
105
130
  const loopbackUrl = z
106
131
  .url()
107
132
  .transform((value) => new URL(value))
@@ -113,9 +138,9 @@ const configSchema = z
113
138
  workspaceRoot: z.string().min(1),
114
139
  host: z.string().min(1).default("127.0.0.1"),
115
140
  port: z.number().int().min(0).max(65_535).default(0),
116
- executionBaseUrl: loopbackUrl,
141
+ executionBaseUrl: loopbackUrl.optional(),
117
142
  executionTransportCredentialFile: z.string().min(1).optional(),
118
- modelBaseUrl: loopbackUrl,
143
+ modelBaseUrl: loopbackUrl.optional(),
119
144
  modelTransportCredentialFile: z.string().min(1).optional(),
120
145
  gatewayTransportCredentialFile: z.string().min(1).optional(),
121
146
  roles: z
@@ -208,10 +233,21 @@ async function responseJson(response) {
208
233
  });
209
234
  return value;
210
235
  }
211
- function createOwnerHttpClient(owner, baseUrl, credential) {
236
+ function createOwnerHttpClient(owner, baseUrl, credential, resolveConnection) {
237
+ const connection = async () => {
238
+ const resolved = resolveConnection
239
+ ? await resolveConnection(owner)
240
+ : baseUrl
241
+ ? { baseUrl, credential }
242
+ : undefined;
243
+ if (!resolved)
244
+ throw new Error(`${owner.toUpperCase()}_SERVICE_UNAVAILABLE`);
245
+ return { ...resolved, baseUrl: loopbackUrl.parse(resolved.baseUrl) };
246
+ };
212
247
  return Object.freeze({
213
248
  async readiness() {
214
249
  try {
250
+ const { baseUrl, credential } = await connection();
215
251
  const response = await fetch(`${baseUrl}/ready`, {
216
252
  headers: credential ? { authorization: `Bearer ${credential}` } : {},
217
253
  signal: AbortSignal.timeout(2_000),
@@ -235,6 +271,7 @@ function createOwnerHttpClient(owner, baseUrl, credential) {
235
271
  }
236
272
  },
237
273
  async invoke(operationId, input) {
274
+ const { baseUrl, credential } = await connection();
238
275
  let path;
239
276
  let method = "POST";
240
277
  let callerContext;
@@ -377,7 +414,7 @@ async function readDeploymentOwnerSummary(stateRoot) {
377
414
  return undefined;
378
415
  }
379
416
  }
380
- async function constructGraph(config, executionCredential, modelCredential) {
417
+ async function constructGraph(config, executionCredential, modelCredential, resolveConnection, resolveLocalToolConnection) {
381
418
  const databasePath = join(config.stateRoot, "state", "task.sqlite");
382
419
  const migration = applyMigrations({
383
420
  databasePath,
@@ -444,8 +481,9 @@ async function constructGraph(config, executionCredential, modelCredential) {
444
481
  taskStore.close();
445
482
  throw error;
446
483
  }
447
- const execution = createOwnerHttpClient("execution", config.executionBaseUrl, executionCredential);
448
- const model = createOwnerHttpClient("model", config.modelBaseUrl, modelCredential);
484
+ const execution = createOwnerHttpClient("execution", config.executionBaseUrl, executionCredential, resolveConnection);
485
+ const model = createOwnerHttpClient("model", config.modelBaseUrl, modelCredential, resolveConnection);
486
+ let reconciliationCoordinator;
449
487
  const boundedSystemView = async (view) => {
450
488
  if (view === "task") {
451
489
  const tasks = unwrap(task.queries.listTasks({})).tasks;
@@ -615,56 +653,6 @@ async function constructGraph(config, executionCredential, modelCredential) {
615
653
  });
616
654
  return binding.workerRef;
617
655
  };
618
- const admitExecutionRead = async (authenticatedRoleRef, rawRecord) => {
619
- const record = object(rawRecord, "execution read record");
620
- if (record.callerRef !== authenticatedRoleRef)
621
- throw Object.assign(new Error("EXECUTION_CALLER_MISMATCH"), {
622
- httpStatus: 403,
623
- });
624
- if (typeof record.taskId === "string") {
625
- if (record.roleRef !== authenticatedRoleRef)
626
- throw Object.assign(new Error("EXECUTION_ROLE_SCOPE_MISMATCH"), {
627
- httpStatus: 403,
628
- });
629
- if (typeof record.workerRef !== "string")
630
- throw Object.assign(new Error("EXECUTION_WORKER_SCOPE_REQUIRED"), {
631
- httpStatus: 403,
632
- });
633
- await admitTaskParticipant(record.taskId, authenticatedRoleRef, record.workerRef);
634
- }
635
- return rawRecord;
636
- };
637
- const admissionError = (code, httpStatus = 403) => {
638
- throw Object.assign(new Error(code), { httpStatus });
639
- };
640
- const admitExactNodeExecution = async (input, authenticatedRoleRef) => {
641
- const taskId = string(input.taskId, "taskId");
642
- const nodeId = string(input.nodeId, "nodeId");
643
- const runNo = positiveInteger(input.runNo, "runNo");
644
- const workerRef = await admitTaskParticipant(taskId, authenticatedRoleRef);
645
- await agent.validateWorker({ authenticatedRoleRef, taskId, workerRef });
646
- const taskFact = taskFacts(taskId);
647
- const nodeContext = unwrap(task.queries.getNodeContext({ taskId, nodeId }));
648
- if (taskFact.status !== "ACTIVE")
649
- admissionError("EXECUTION_TASK_NOT_ACTIVE");
650
- if (taskFact.currentNodeId !== nodeId)
651
- admissionError("EXECUTION_NODE_NOT_CURRENT");
652
- if (nodeContext.node.status !== "IN_PROGRESS")
653
- admissionError("EXECUTION_NODE_NOT_RUNNING");
654
- if (nodeContext.node.runNo !== runNo)
655
- admissionError("EXECUTION_GENERATION_MISMATCH");
656
- const nodeBinding = taskFact.roleBindings.find((binding) => binding.agentPackageRef === nodeContext.node.requiredAgentPackageRef);
657
- if (!nodeBinding)
658
- throw Object.assign(new Error("TASK_ROLE_BINDING_REQUIRED"), {
659
- httpStatus: 403,
660
- });
661
- if (nodeBinding.roleRef !== authenticatedRoleRef)
662
- admissionError("EXECUTION_ROLE_SCOPE_MISMATCH");
663
- if (nodeBinding.workerRef !== workerRef ||
664
- nodeContext.node.workerRef !== workerRef)
665
- admissionError("EXECUTION_WORKER_SCOPE_MISMATCH");
666
- return { taskId, nodeId, runNo, workerRef };
667
- };
668
656
  const route = async (operationId, authenticatedRoleRef, rawInput, context) => {
669
657
  const role = agent.getRegisteredRole(authenticatedRoleRef);
670
658
  if (!roleOperations[role.agentPackageRef]?.has(operationId))
@@ -676,6 +664,63 @@ async function constructGraph(config, executionCredential, modelCredential) {
676
664
  return agent.askPeer({ ...input, authenticatedRoleRef });
677
665
  if (operationId === "replyPeer")
678
666
  return agent.replyPeer({ ...input, authenticatedRoleRef });
667
+ if (directToolActionIds.includes(operationId)) {
668
+ const tool = operationId;
669
+ const keys = Object.keys(input).sort();
670
+ if (keys.length !== 2 || keys[0] !== "input" || keys[1] !== "operation")
671
+ throw Object.assign(new Error("DIRECT_TOOL_INPUT_INVALID"), {
672
+ httpStatus: 400,
673
+ });
674
+ const toolOperation = string(input.operation, "operation");
675
+ const toolInput = object(input.input, "input");
676
+ assertNoDirectToolPlatformIdentity(toolInput);
677
+ if (!roleAllowsDirectToolOperation(role.agentPackageRef, operationId, toolOperation, toolInput))
678
+ throw Object.assign(new Error("ROLE_TOOL_OPERATION_DENIED"), {
679
+ httpStatus: 403,
680
+ });
681
+ const deadlineAt = context?.deadlineAt;
682
+ if (typeof deadlineAt !== "string" ||
683
+ Number.isNaN(Date.parse(deadlineAt)))
684
+ throw Object.assign(new Error("DIRECT_TOOL_DEADLINE_REQUIRED"), {
685
+ httpStatus: 400,
686
+ });
687
+ const connection = await resolveLocalToolConnection?.();
688
+ if (!connection)
689
+ throw Object.assign(new Error("LOCAL_TOOL_BRIDGE_UNAVAILABLE"), {
690
+ httpStatus: 503,
691
+ });
692
+ try {
693
+ return await createLocalToolBridgeHostClient({
694
+ endpoint: connection.endpoint,
695
+ token: connection.credential,
696
+ }).request({
697
+ authenticatedRoleRef,
698
+ workspaceRoot: config.workspaceRoot,
699
+ tool,
700
+ operation: toolOperation,
701
+ input: toolInput,
702
+ deadlineAt,
703
+ });
704
+ }
705
+ catch (error) {
706
+ const code = error instanceof LocalToolBridgeError
707
+ ? error.code
708
+ : "LOCAL_TOOL_BRIDGE_UNAVAILABLE";
709
+ const httpStatus = code === "LOCAL_TOOL_COMMAND_TIMEOUT"
710
+ ? 504
711
+ : code === "LOCAL_TOOL_RESULT_UNKNOWN"
712
+ ? 409
713
+ : code === "LOCAL_TOOL_SCOPE_DENIED"
714
+ ? 403
715
+ : code === "LOCAL_TOOL_OFFLINE" ||
716
+ code === "LOCAL_TOOL_PROVIDER_UNAVAILABLE"
717
+ ? 503
718
+ : code === "LOCAL_TOOL_COMMAND_FAILED"
719
+ ? 500
720
+ : 400;
721
+ throw Object.assign(new Error(code), { httpStatus });
722
+ }
723
+ }
679
724
  const taskOperation = taskOperations.get(operationId);
680
725
  if (taskOperation) {
681
726
  let actorRef = authenticatedRoleRef;
@@ -722,65 +767,13 @@ async function constructGraph(config, executionCredential, modelCredential) {
722
767
  const taskResult = await taskOperation(queryOperations.has(operationId)
723
768
  ? canonicalTaskInput
724
769
  : { ...canonicalTaskInput, actorRef });
770
+ if (taskMutationOperations.has(operationId) &&
771
+ typeof input.taskId === "string")
772
+ reconciliationCoordinator?.kick(input.taskId);
725
773
  if (operationId === "getTaskDocument")
726
774
  return fileBridgeOutputForTaskResult(taskResult);
727
- if (operationId === "getNodeContext" &&
728
- roleOperations[role.agentPackageRef]?.has("executeCapability")) {
729
- const nodeContext = object(taskResult, "node context result");
730
- const nodeContextData = object(nodeContext.data, "node context data");
731
- const taskContext = object(nodeContextData.task, "node context task");
732
- const executionNodeContext = object(nodeContextData.node, "node context node");
733
- return {
734
- ...nodeContext,
735
- executionCapabilityIds: [...executionCapabilityIds],
736
- executionRequestContext: {
737
- contract: "execution",
738
- contractVersion: "1.0.0",
739
- taskId: string(taskContext.taskId, "task.taskId"),
740
- nodeId: string(executionNodeContext.nodeId, "node.nodeId"),
741
- runNo: positiveInteger(executionNodeContext.runNo, "node.runNo"),
742
- },
743
- };
744
- }
745
775
  return taskResult;
746
776
  }
747
- if (operationId === "executeCapability") {
748
- const scope = await admitExactNodeExecution(input, authenticatedRoleRef);
749
- const { workerRef: _suppliedWorkerRef, projectRoot: _suppliedProjectRoot, roleRef: _suppliedRoleRef, callerRef: _suppliedCallerRef, ...ownerInput } = input;
750
- const result = await execution.invoke(operationId, {
751
- ...ownerInput,
752
- callerRef: authenticatedRoleRef,
753
- roleRef: authenticatedRoleRef,
754
- taskId: scope.taskId,
755
- nodeId: scope.nodeId,
756
- runNo: scope.runNo,
757
- workerRef: scope.workerRef,
758
- });
759
- // A normal Action may complete synchronously inside the current Worker Turn.
760
- // Do not manufacture a Browser RESUME for every terminal Execution record.
761
- // Only a future explicit async-completion signal may emit EXECUTION_RESULT_READY.
762
- return result;
763
- }
764
- if (operationId === "getExecution") {
765
- const record = await execution.invoke(operationId, {
766
- ...input,
767
- callerRef: authenticatedRoleRef,
768
- });
769
- return admitExecutionRead(authenticatedRoleRef, record);
770
- }
771
- if (operationId === "readExecutionOutput") {
772
- const record = await execution.invoke("getExecution", {
773
- contract: "execution",
774
- contractVersion: "1.0.0",
775
- executionRef: string(input.executionRef, "executionRef"),
776
- callerRef: authenticatedRoleRef,
777
- });
778
- await admitExecutionRead(authenticatedRoleRef, record);
779
- return execution.invoke(operationId, {
780
- ...input,
781
- callerRef: authenticatedRoleRef,
782
- });
783
- }
784
777
  throw new Error("OPERATION_NOT_ROUTED");
785
778
  };
786
779
  const browserOwnerPorts = Object.freeze({
@@ -813,6 +806,7 @@ async function constructGraph(config, executionCredential, modelCredential) {
813
806
  actorRef: "platform-host:worker-provisioning",
814
807
  idempotencyKey: `browser-bind:${binding.taskId}:${binding.roleRef}:${binding.workerRef}`,
815
808
  }));
809
+ reconciliationCoordinator?.kick(binding.taskId);
816
810
  },
817
811
  }),
818
812
  agent: Object.freeze({
@@ -861,7 +855,7 @@ async function constructGraph(config, executionCredential, modelCredential) {
861
855
  request.capability === "worker.wake" ||
862
856
  request.capability === "collaboration.deliver";
863
857
  const internalBrowserCaller = request.callerRef === "platform-host:carrier-controller" ||
864
- request.callerRef === "extension:task-observer" ||
858
+ request.callerRef === "platform-host:task-reconciliation" ||
865
859
  request.callerRef === "extension:collaboration-carrier";
866
860
  if (!internalBrowserCaller)
867
861
  agent.getRegisteredRole(request.callerRef);
@@ -1131,7 +1125,7 @@ async function constructGraph(config, executionCredential, modelCredential) {
1131
1125
  // Dispatch all three fixed Workers concurrently. When `waitFor` names a
1132
1126
  // subset of roleRefs (the J1 Product path), only those results gate the
1133
1127
  // return; the remaining Worker creation is a durable, idempotent Execution
1134
- // effect whose completion the ensureWorkers recovery reconciles from the
1128
+ // effect whose completion backend reconciliation recovers from the
1135
1129
  // durable Task binding facts — never a bare in-memory promise.
1136
1130
  const waitFor = new Set(options?.waitFor ?? []);
1137
1131
  const shouldWait = (agentPackageRef) => waitFor.size === 0 ||
@@ -1145,7 +1139,7 @@ async function constructGraph(config, executionCredential, modelCredential) {
1145
1139
  for (const entry of deferred) {
1146
1140
  entry.promise.catch(() => {
1147
1141
  // Deferred Worker creation failure is recoverable: the durable
1148
- // binding stays unset, so a later ensureWorkers pass re-provisions
1142
+ // binding stays unset, so a later backend reconciliation pass re-provisions
1149
1143
  // only the missing role.
1150
1144
  });
1151
1145
  }
@@ -1155,6 +1149,67 @@ async function constructGraph(config, executionCredential, modelCredential) {
1155
1149
  throw failure.reason;
1156
1150
  return unwrap(task.queries.getTask({ taskId }));
1157
1151
  };
1152
+ const requestTaskWake = async (input) => {
1153
+ const underlyingRef = input.underlyingRef ?? "none";
1154
+ const wakeExecution = object(await execution.invoke("executeCapability", {
1155
+ contract: "execution",
1156
+ contractVersion: "1.0.0",
1157
+ idempotencyKey: `task-reconciliation-wake:${input.taskId}:${input.nodeId}:${input.runNo}:${input.trigger}:${underlyingRef}`,
1158
+ callerRef: "platform-host:task-reconciliation",
1159
+ correlationId: `task-reconciliation:${input.taskId}:${input.nodeId}:${input.runNo}`,
1160
+ taskId: input.taskId,
1161
+ nodeId: input.nodeId,
1162
+ runNo: input.runNo,
1163
+ roleRef: input.roleRef,
1164
+ workerRef: input.workerRef,
1165
+ capability: "worker.wake",
1166
+ input: {
1167
+ roleRef: input.roleRef,
1168
+ workerRef: input.workerRef,
1169
+ taskId: input.taskId,
1170
+ nodeId: input.nodeId,
1171
+ runNo: input.runNo,
1172
+ trigger: input.trigger,
1173
+ fingerprint: `wake:${input.taskId}:${input.nodeId}:${input.runNo}:${input.trigger}:${underlyingRef}`,
1174
+ },
1175
+ }), "task reconciliation wake execution");
1176
+ if (wakeExecution.status !== "SUCCEEDED" ||
1177
+ wakeExecution.sideEffectState !== "APPLIED")
1178
+ throw new Error(`TASK_WAKE_NOT_CONFIRMED:${String(wakeExecution.status)}:${String(wakeExecution.sideEffectState)}`);
1179
+ return wakeExecution;
1180
+ };
1181
+ reconciliationCoordinator = createReconciliationCoordinator({
1182
+ async listTaskPage(input) {
1183
+ return taskStore.listReconciliationTaskIds({
1184
+ statuses: [
1185
+ "PENDING",
1186
+ "READY",
1187
+ "ACTIVE",
1188
+ "WAITING",
1189
+ "FAILED",
1190
+ "PAUSED",
1191
+ ],
1192
+ ...input,
1193
+ });
1194
+ },
1195
+ async listExecutionSignals() {
1196
+ const batch = object(await execution.invoke("listExecutionObserverSignals", {
1197
+ limit: 100,
1198
+ consumer: "task-reconciliation",
1199
+ }), "execution observer signals");
1200
+ return Array.isArray(batch.signals) ? batch.signals : [];
1201
+ },
1202
+ async acknowledgeExecutionSignal(signalRef) {
1203
+ await execution.invoke("acknowledgeExecutionObserverSignal", {
1204
+ signalRef,
1205
+ });
1206
+ },
1207
+ async ensureWorkers(taskId) {
1208
+ await ensureTaskWorkers(taskId);
1209
+ },
1210
+ getProjection: (taskId) => taskDriverPorts.getTaskDriveProjection(taskId),
1211
+ requestWake: requestTaskWake,
1212
+ });
1158
1213
  const taskApplication = Object.freeze({
1159
1214
  async invoke(operation, rawInput) {
1160
1215
  const value = object(rawInput, "task application input");
@@ -1185,12 +1240,12 @@ async function constructGraph(config, executionCredential, modelCredential) {
1185
1240
  // J1: return once the Product Worker is durably bound; Dev/Test
1186
1241
  // continue as recoverable durable effects without blocking Product
1187
1242
  // requirement discussion.
1188
- return ensureTaskWorkers(created.taskId, {
1243
+ const provisioned = await ensureTaskWorkers(created.taskId, {
1189
1244
  waitFor: [roleForPackage("@tomflow/proflow-agent-product").roleRef],
1190
1245
  });
1246
+ reconciliationCoordinator?.kick(created.taskId);
1247
+ return provisioned;
1191
1248
  }
1192
- if (operation === "task.ensureWorkers")
1193
- return ensureTaskWorkers(string(value.taskId, "taskId"));
1194
1249
  if (operation === "task.list")
1195
1250
  return unwrap(task.queries.listTasks({
1196
1251
  ...(Array.isArray(value.statuses)
@@ -1199,13 +1254,17 @@ async function constructGraph(config, executionCredential, modelCredential) {
1199
1254
  }));
1200
1255
  if (operation === "task.get")
1201
1256
  return unwrap(task.queries.getTask({ taskId: string(value.taskId, "taskId") }));
1202
- if (operation === "task.start")
1203
- return unwrap(task.commands.startTask({
1204
- taskId: string(value.taskId, "taskId"),
1257
+ if (operation === "task.start") {
1258
+ const taskId = string(value.taskId, "taskId");
1259
+ const result = unwrap(task.commands.startTask({
1260
+ taskId,
1205
1261
  expectedTaskVersion: Number(value.expectedTaskVersion),
1206
1262
  actorRef: "extension:human",
1207
1263
  idempotencyKey: string(value.idempotencyKey, "idempotencyKey"),
1208
1264
  }));
1265
+ reconciliationCoordinator?.kick(taskId);
1266
+ return result;
1267
+ }
1209
1268
  if (operation === "message.acknowledge")
1210
1269
  return unwrap(task.commands.acknowledgeMessage({
1211
1270
  messageId: string(value.messageId, "messageId"),
@@ -1215,25 +1274,62 @@ async function constructGraph(config, executionCredential, modelCredential) {
1215
1274
  actorRef: "extension:human",
1216
1275
  idempotencyKey: string(value.idempotencyKey, "idempotencyKey"),
1217
1276
  }));
1218
- if (operation === "task.resume")
1219
- return unwrap(task.commands.resumeTask({
1220
- taskId: string(value.taskId, "taskId"),
1277
+ if (operation === "task.resume") {
1278
+ const taskId = string(value.taskId, "taskId");
1279
+ const result = unwrap(task.commands.resumeTask({
1280
+ taskId,
1221
1281
  expectedTaskVersion: positiveInteger(value.expectedTaskVersion, "expectedTaskVersion"),
1222
1282
  actorRef: "extension:human",
1223
1283
  idempotencyKey: string(value.idempotencyKey, "idempotencyKey"),
1224
1284
  }));
1225
- if (operation === "node.reopen")
1226
- return unwrap(task.commands.reopenNode({
1227
- taskId: string(value.taskId, "taskId"),
1285
+ reconciliationCoordinator?.kick(taskId);
1286
+ return result;
1287
+ }
1288
+ if (operation === "node.reopen") {
1289
+ const taskId = string(value.taskId, "taskId");
1290
+ const result = unwrap(task.commands.reopenNode({
1291
+ taskId,
1228
1292
  nodeId: string(value.nodeId, "nodeId"),
1229
1293
  reason: string(value.reason, "reason"),
1230
1294
  expectedTaskVersion: Number(value.expectedTaskVersion),
1231
1295
  actorRef: "extension:human",
1232
1296
  idempotencyKey: string(value.idempotencyKey, "idempotencyKey"),
1233
1297
  }));
1298
+ reconciliationCoordinator?.kick(taskId);
1299
+ return result;
1300
+ }
1234
1301
  throw new Error("UNSUPPORTED_TASK_APPLICATION_OPERATION");
1235
1302
  },
1236
1303
  });
1304
+ async function approvalExecutionContext(approvalRef) {
1305
+ const approval = object(await execution.invoke("getExecutionApproval", { approvalRef }), "approval fact");
1306
+ const executionFact = object(await execution.invoke("getExecution", {
1307
+ executionRef: string(approval.executionRef, "executionRef"),
1308
+ callerRef: string(approval.callerRef, "callerRef"),
1309
+ }), "execution fact");
1310
+ const runNo = Number(executionFact.runNo);
1311
+ if (!Number.isInteger(runNo) || runNo <= 0)
1312
+ throw new Error("EXECUTION_RUN_GENERATION_REQUIRED");
1313
+ return {
1314
+ executionRef: string(executionFact.executionRef, "executionRef"),
1315
+ taskId: string(executionFact.taskId, "taskId"),
1316
+ nodeId: string(executionFact.nodeId, "nodeId"),
1317
+ runNo,
1318
+ roleRef: string(executionFact.roleRef, "roleRef"),
1319
+ workerRef: string(executionFact.workerRef, "workerRef"),
1320
+ };
1321
+ }
1322
+ const reconcileApproval = async (approvalRef) => {
1323
+ const context = await approvalExecutionContext(approvalRef);
1324
+ reconciliationCoordinator?.kick(context.taskId, {
1325
+ trigger: "RECOVERY_RESUME",
1326
+ ref: approvalRef,
1327
+ targetWorkerRef: context.workerRef,
1328
+ nodeId: context.nodeId,
1329
+ runNo: context.runNo,
1330
+ });
1331
+ return context;
1332
+ };
1237
1333
  const approvalApplication = Object.freeze({
1238
1334
  async invoke(operation, rawInput) {
1239
1335
  const value = object(rawInput, "approval application input");
@@ -1248,52 +1344,42 @@ async function constructGraph(config, executionCredential, modelCredential) {
1248
1344
  ...value,
1249
1345
  actorRef: "extension:human",
1250
1346
  });
1251
- if (operation === "approval.allow" || operation === "approval.deny")
1252
- return execution.invoke("decideExecutionApproval", {
1347
+ if (operation === "approval.allow" || operation === "approval.deny") {
1348
+ const approvalRef = string(value.approvalRef, "approvalRef");
1349
+ const result = await execution.invoke("decideExecutionApproval", {
1253
1350
  contract: "execution.approval",
1254
1351
  contractVersion: "1.0.0",
1255
- approvalRef: string(value.approvalRef, "approvalRef"),
1352
+ approvalRef,
1256
1353
  actorRef: "extension:human",
1257
1354
  expectedVersion: Number(value.expectedVersion),
1258
1355
  decision: operation === "approval.allow" ? "ALLOW" : "DENY",
1259
1356
  ...(typeof value.reason === "string" ? { reason: value.reason } : {}),
1260
1357
  });
1261
- if (operation === "approval.revoke")
1262
- return execution.invoke("revokeExecutionApproval", {
1358
+ await reconcileApproval(approvalRef);
1359
+ return result;
1360
+ }
1361
+ if (operation === "approval.revoke") {
1362
+ const approvalRef = string(value.approvalRef, "approvalRef");
1363
+ const result = await execution.invoke("revokeExecutionApproval", {
1263
1364
  contract: "execution.approval",
1264
1365
  contractVersion: "1.0.0",
1265
- approvalRef: string(value.approvalRef, "approvalRef"),
1366
+ approvalRef,
1266
1367
  actorRef: "extension:human",
1267
1368
  expectedVersion: Number(value.expectedVersion),
1268
1369
  reason: string(value.reason, "reason"),
1269
1370
  });
1371
+ await reconcileApproval(approvalRef);
1372
+ return result;
1373
+ }
1270
1374
  throw new Error("UNSUPPORTED_APPROVAL_APPLICATION_OPERATION");
1271
1375
  },
1272
1376
  });
1273
1377
  const observerApplication = Object.freeze({
1274
1378
  async invoke(operation, rawInput) {
1275
1379
  const value = object(rawInput, "observer application input");
1276
- if (operation === "task.projection")
1277
- return taskDriverPorts.getTaskDriveProjection(string(value.taskId, "taskId"));
1278
- if (operation === "approval.executionContext") {
1279
- const approval = object(await execution.invoke("getExecutionApproval", {
1280
- approvalRef: string(value.approvalRef, "approvalRef"),
1281
- }), "approval fact");
1282
- const executionFact = object(await execution.invoke("getExecution", {
1283
- executionRef: string(approval.executionRef, "executionRef"),
1284
- callerRef: string(approval.callerRef, "callerRef"),
1285
- }), "execution fact");
1286
- const runNo = Number(executionFact.runNo);
1287
- if (!Number.isInteger(runNo) || runNo <= 0)
1288
- throw new Error("EXECUTION_RUN_GENERATION_REQUIRED");
1289
- return {
1290
- executionRef: string(executionFact.executionRef, "executionRef"),
1291
- taskId: string(executionFact.taskId, "taskId"),
1292
- nodeId: string(executionFact.nodeId, "nodeId"),
1293
- runNo,
1294
- roleRef: string(executionFact.roleRef, "roleRef"),
1295
- workerRef: string(executionFact.workerRef, "workerRef"),
1296
- };
1380
+ if (operation === "task.reconcileAll") {
1381
+ void reconciliationCoordinator?.sweep();
1382
+ return { scheduled: true };
1297
1383
  }
1298
1384
  if (operation === "browser.permission.classify") {
1299
1385
  const roleRef = string(value.roleRef, "roleRef");
@@ -1378,79 +1464,6 @@ async function constructGraph(config, executionCredential, modelCredential) {
1378
1464
  });
1379
1465
  return { reported: true };
1380
1466
  }
1381
- if (operation === "execution.listSignals")
1382
- return execution.invoke("listExecutionObserverSignals", {
1383
- limit: Number(value.limit ?? 50),
1384
- });
1385
- if (operation === "execution.ackSignal")
1386
- return execution.invoke("acknowledgeExecutionObserverSignal", {
1387
- signalRef: string(value.signalRef, "signalRef"),
1388
- });
1389
- if (operation === "task.wake") {
1390
- const taskId = string(value.taskId, "taskId");
1391
- const nodeId = string(value.nodeId, "nodeId");
1392
- const runNo = Number(value.runNo);
1393
- const roleRef = string(value.roleRef, "roleRef");
1394
- const workerRef = string(value.workerRef, "workerRef");
1395
- const trigger = string(value.trigger, "trigger");
1396
- const underlyingRef = typeof value.underlyingRef === "string"
1397
- ? value.underlyingRef
1398
- : "none";
1399
- const wakeExecution = object(await execution.invoke("executeCapability", {
1400
- contract: "execution",
1401
- contractVersion: "1.0.0",
1402
- idempotencyKey: `task-observer-wake:${taskId}:${nodeId}:${runNo}:${trigger}:${underlyingRef}`,
1403
- callerRef: "extension:task-observer",
1404
- correlationId: `task-observer:${taskId}:${nodeId}:${runNo}`,
1405
- taskId,
1406
- nodeId,
1407
- runNo,
1408
- roleRef,
1409
- workerRef,
1410
- capability: "worker.wake",
1411
- input: {
1412
- roleRef,
1413
- workerRef,
1414
- taskId,
1415
- nodeId,
1416
- runNo,
1417
- trigger,
1418
- fingerprint: `wake:${taskId}:${nodeId}:${runNo}:${trigger}:${underlyingRef}`,
1419
- },
1420
- }), "task wake execution");
1421
- if (wakeExecution.status !== "SUCCEEDED" ||
1422
- wakeExecution.sideEffectState !== "APPLIED")
1423
- throw new Error(`TASK_WAKE_NOT_CONFIRMED:${String(wakeExecution.status)}:${String(wakeExecution.sideEffectState)}`);
1424
- return wakeExecution;
1425
- }
1426
- if (operation === "task.diagnostic") {
1427
- const response = object(await model.invoke("infer", {
1428
- contractVersion: "1.0.0",
1429
- specRef: "task.diagnostic.v1",
1430
- mode: "reason",
1431
- priority: "business",
1432
- trace: {
1433
- callerRef: "extension:task-observer",
1434
- correlationId: string(value.correlationId, "correlationId"),
1435
- taskId: string(value.taskId, "taskId"),
1436
- nodeId: string(value.nodeId, "nodeId"),
1437
- },
1438
- payload: value.payload,
1439
- }), "task diagnostic inference");
1440
- if (response.status !== "SUCCEEDED") {
1441
- const error = typeof response.error === "object" && response.error !== null
1442
- ? response.error
1443
- : {};
1444
- const code = error.code === "CONTEXT_TOO_LARGE"
1445
- ? "CONTEXT_TOO_LARGE"
1446
- : error.code === "MODEL_UNAVAILABLE" ||
1447
- error.code === "CAPABILITY_UNSUPPORTED"
1448
- ? "REASON_UNAVAILABLE"
1449
- : "REASON_FAILED";
1450
- return { ok: false, errorCode: code };
1451
- }
1452
- return taskDiagnosticReasonResultSchema.parse(response.data);
1453
- }
1454
1467
  if (operation === "system.view")
1455
1468
  return boundedSystemView(string(value.view, "view"));
1456
1469
  if (operation === "system.drilldown") {
@@ -1503,6 +1516,7 @@ async function constructGraph(config, executionCredential, modelCredential) {
1503
1516
  throw new Error("UNSUPPORTED_OBSERVER_APPLICATION_OPERATION");
1504
1517
  },
1505
1518
  });
1519
+ reconciliationCoordinator.start();
1506
1520
  return Object.freeze({
1507
1521
  route,
1508
1522
  browserOwnerPorts,
@@ -1513,30 +1527,10 @@ async function constructGraph(config, executionCredential, modelCredential) {
1513
1527
  taskApplication,
1514
1528
  approvalApplication,
1515
1529
  observerApplication,
1516
- async lookup(operationId, authenticatedRoleRef, input) {
1517
- const value = object(input, "lookup input");
1518
- if (operationId === "executeCapability") {
1519
- if (value.executionRef) {
1520
- const record = await execution.invoke("getExecution", {
1521
- ...value,
1522
- callerRef: authenticatedRoleRef,
1523
- });
1524
- return admitExecutionRead(authenticatedRoleRef, record);
1525
- }
1526
- const scope = await admitExactNodeExecution(value, authenticatedRoleRef);
1527
- const { workerRef: _suppliedWorkerRef, projectRoot: _suppliedProjectRoot, roleRef: _suppliedRoleRef, callerRef: _suppliedCallerRef, ...ownerInput } = value;
1528
- const record = await execution.invoke("lookupExecutionIntent", {
1529
- ...ownerInput,
1530
- callerRef: authenticatedRoleRef,
1531
- roleRef: authenticatedRoleRef,
1532
- taskId: scope.taskId,
1533
- nodeId: scope.nodeId,
1534
- runNo: scope.runNo,
1535
- workerRef: scope.workerRef,
1536
- });
1537
- return admitExecutionRead(authenticatedRoleRef, record);
1538
- }
1539
- return route(operationId, authenticatedRoleRef, value);
1530
+ async lookup(_operationId, _authenticatedRoleRef, _input) {
1531
+ throw Object.assign(new Error("ACTION_RESULT_LOOKUP_UNSUPPORTED"), {
1532
+ httpStatus: 409,
1533
+ });
1540
1534
  },
1541
1535
  async readiness() {
1542
1536
  const diagnostics = taskStore.diagnostics();
@@ -1565,6 +1559,7 @@ async function constructGraph(config, executionCredential, modelCredential) {
1565
1559
  };
1566
1560
  },
1567
1561
  close() {
1562
+ reconciliationCoordinator?.stop();
1568
1563
  agent.close();
1569
1564
  taskStore.close();
1570
1565
  },
@@ -1721,7 +1716,7 @@ export function createPlatformHost(input) {
1721
1716
  liveness: lifecycle === "STOPPED" ? "DOWN" : "UP",
1722
1717
  transport: server ? "UP" : "DOWN",
1723
1718
  readiness: accepting &&
1724
- Object.values(dependencies).every((item) => item.status === "READY")
1719
+ [dependencies.task, dependencies.agent].every((item) => item.status === "READY")
1725
1720
  ? "READY"
1726
1721
  : "NOT_READY",
1727
1722
  accepting,
@@ -1820,8 +1815,7 @@ export function createPlatformHost(input) {
1820
1815
  log("DEPENDENCY_INITIALIZATION_STARTED", {
1821
1816
  order: ["task", "agent", "execution-client", "model-client"],
1822
1817
  });
1823
- graph = await constructGraph(input.config, executionTransportCredential, modelTransportCredential);
1824
- await graph.readiness();
1818
+ graph = await constructGraph(input.config, executionTransportCredential, modelTransportCredential, input.resolveOwnerConnection, input.resolveLocalToolConnection);
1825
1819
  server = createServer((request, response) => {
1826
1820
  const work = (async () => {
1827
1821
  const url = new URL(request.url ?? "/", "http://platform-host.local");
@@ -2043,11 +2037,16 @@ export function createPlatformHost(input) {
2043
2037
  const operationId = decodeURIComponent(lookup ? suffix.slice(0, -"/result".length) : suffix);
2044
2038
  const result = lookup
2045
2039
  ? await graph.lookup(operationId, authenticatedRoleRef, body.input)
2046
- : await graph.route(operationId, authenticatedRoleRef, body.input, body.fileMaterializationInputs === undefined
2047
- ? undefined
2048
- : {
2049
- fileMaterializationInputs: body.fileMaterializationInputs,
2050
- });
2040
+ : await graph.route(operationId, authenticatedRoleRef, body.input, {
2041
+ ...(body.fileMaterializationInputs === undefined
2042
+ ? {}
2043
+ : {
2044
+ fileMaterializationInputs: body.fileMaterializationInputs,
2045
+ }),
2046
+ ...(typeof body.deadlineAt === "string"
2047
+ ? { deadlineAt: body.deadlineAt }
2048
+ : {}),
2049
+ });
2051
2050
  respond(response, 200, result);
2052
2051
  }
2053
2052
  catch (error) {