@tomflow/proflow-platform-host 0.1.22 → 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, executionCapabilityInputJsonSchemas, } 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,66 +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
- executionCapabilityInputSchemas: executionCapabilityInputJsonSchemas,
737
- executionRequestContext: {
738
- contract: "execution",
739
- contractVersion: "1.0.0",
740
- taskId: string(taskContext.taskId, "task.taskId"),
741
- nodeId: string(executionNodeContext.nodeId, "node.nodeId"),
742
- runNo: positiveInteger(executionNodeContext.runNo, "node.runNo"),
743
- },
744
- };
745
- }
746
775
  return taskResult;
747
776
  }
748
- if (operationId === "executeCapability") {
749
- const scope = await admitExactNodeExecution(input, authenticatedRoleRef);
750
- const { workerRef: _suppliedWorkerRef, projectRoot: _suppliedProjectRoot, roleRef: _suppliedRoleRef, callerRef: _suppliedCallerRef, ...ownerInput } = input;
751
- const result = await execution.invoke(operationId, {
752
- ...ownerInput,
753
- callerRef: authenticatedRoleRef,
754
- roleRef: authenticatedRoleRef,
755
- taskId: scope.taskId,
756
- nodeId: scope.nodeId,
757
- runNo: scope.runNo,
758
- workerRef: scope.workerRef,
759
- });
760
- // A normal Action may complete synchronously inside the current Worker Turn.
761
- // Do not manufacture a Browser RESUME for every terminal Execution record.
762
- // Only a future explicit async-completion signal may emit EXECUTION_RESULT_READY.
763
- return result;
764
- }
765
- if (operationId === "getExecution") {
766
- const record = await execution.invoke(operationId, {
767
- ...input,
768
- callerRef: authenticatedRoleRef,
769
- });
770
- return admitExecutionRead(authenticatedRoleRef, record);
771
- }
772
- if (operationId === "readExecutionOutput") {
773
- const record = await execution.invoke("getExecution", {
774
- contract: "execution",
775
- contractVersion: "1.0.0",
776
- executionRef: string(input.executionRef, "executionRef"),
777
- callerRef: authenticatedRoleRef,
778
- });
779
- await admitExecutionRead(authenticatedRoleRef, record);
780
- return execution.invoke(operationId, {
781
- ...input,
782
- callerRef: authenticatedRoleRef,
783
- });
784
- }
785
777
  throw new Error("OPERATION_NOT_ROUTED");
786
778
  };
787
779
  const browserOwnerPorts = Object.freeze({
@@ -814,6 +806,7 @@ async function constructGraph(config, executionCredential, modelCredential) {
814
806
  actorRef: "platform-host:worker-provisioning",
815
807
  idempotencyKey: `browser-bind:${binding.taskId}:${binding.roleRef}:${binding.workerRef}`,
816
808
  }));
809
+ reconciliationCoordinator?.kick(binding.taskId);
817
810
  },
818
811
  }),
819
812
  agent: Object.freeze({
@@ -862,7 +855,7 @@ async function constructGraph(config, executionCredential, modelCredential) {
862
855
  request.capability === "worker.wake" ||
863
856
  request.capability === "collaboration.deliver";
864
857
  const internalBrowserCaller = request.callerRef === "platform-host:carrier-controller" ||
865
- request.callerRef === "extension:task-observer" ||
858
+ request.callerRef === "platform-host:task-reconciliation" ||
866
859
  request.callerRef === "extension:collaboration-carrier";
867
860
  if (!internalBrowserCaller)
868
861
  agent.getRegisteredRole(request.callerRef);
@@ -1132,7 +1125,7 @@ async function constructGraph(config, executionCredential, modelCredential) {
1132
1125
  // Dispatch all three fixed Workers concurrently. When `waitFor` names a
1133
1126
  // subset of roleRefs (the J1 Product path), only those results gate the
1134
1127
  // return; the remaining Worker creation is a durable, idempotent Execution
1135
- // effect whose completion the ensureWorkers recovery reconciles from the
1128
+ // effect whose completion backend reconciliation recovers from the
1136
1129
  // durable Task binding facts — never a bare in-memory promise.
1137
1130
  const waitFor = new Set(options?.waitFor ?? []);
1138
1131
  const shouldWait = (agentPackageRef) => waitFor.size === 0 ||
@@ -1146,7 +1139,7 @@ async function constructGraph(config, executionCredential, modelCredential) {
1146
1139
  for (const entry of deferred) {
1147
1140
  entry.promise.catch(() => {
1148
1141
  // Deferred Worker creation failure is recoverable: the durable
1149
- // binding stays unset, so a later ensureWorkers pass re-provisions
1142
+ // binding stays unset, so a later backend reconciliation pass re-provisions
1150
1143
  // only the missing role.
1151
1144
  });
1152
1145
  }
@@ -1156,6 +1149,67 @@ async function constructGraph(config, executionCredential, modelCredential) {
1156
1149
  throw failure.reason;
1157
1150
  return unwrap(task.queries.getTask({ taskId }));
1158
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
+ });
1159
1213
  const taskApplication = Object.freeze({
1160
1214
  async invoke(operation, rawInput) {
1161
1215
  const value = object(rawInput, "task application input");
@@ -1186,12 +1240,12 @@ async function constructGraph(config, executionCredential, modelCredential) {
1186
1240
  // J1: return once the Product Worker is durably bound; Dev/Test
1187
1241
  // continue as recoverable durable effects without blocking Product
1188
1242
  // requirement discussion.
1189
- return ensureTaskWorkers(created.taskId, {
1243
+ const provisioned = await ensureTaskWorkers(created.taskId, {
1190
1244
  waitFor: [roleForPackage("@tomflow/proflow-agent-product").roleRef],
1191
1245
  });
1246
+ reconciliationCoordinator?.kick(created.taskId);
1247
+ return provisioned;
1192
1248
  }
1193
- if (operation === "task.ensureWorkers")
1194
- return ensureTaskWorkers(string(value.taskId, "taskId"));
1195
1249
  if (operation === "task.list")
1196
1250
  return unwrap(task.queries.listTasks({
1197
1251
  ...(Array.isArray(value.statuses)
@@ -1200,13 +1254,17 @@ async function constructGraph(config, executionCredential, modelCredential) {
1200
1254
  }));
1201
1255
  if (operation === "task.get")
1202
1256
  return unwrap(task.queries.getTask({ taskId: string(value.taskId, "taskId") }));
1203
- if (operation === "task.start")
1204
- return unwrap(task.commands.startTask({
1205
- 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,
1206
1261
  expectedTaskVersion: Number(value.expectedTaskVersion),
1207
1262
  actorRef: "extension:human",
1208
1263
  idempotencyKey: string(value.idempotencyKey, "idempotencyKey"),
1209
1264
  }));
1265
+ reconciliationCoordinator?.kick(taskId);
1266
+ return result;
1267
+ }
1210
1268
  if (operation === "message.acknowledge")
1211
1269
  return unwrap(task.commands.acknowledgeMessage({
1212
1270
  messageId: string(value.messageId, "messageId"),
@@ -1216,25 +1274,62 @@ async function constructGraph(config, executionCredential, modelCredential) {
1216
1274
  actorRef: "extension:human",
1217
1275
  idempotencyKey: string(value.idempotencyKey, "idempotencyKey"),
1218
1276
  }));
1219
- if (operation === "task.resume")
1220
- return unwrap(task.commands.resumeTask({
1221
- 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,
1222
1281
  expectedTaskVersion: positiveInteger(value.expectedTaskVersion, "expectedTaskVersion"),
1223
1282
  actorRef: "extension:human",
1224
1283
  idempotencyKey: string(value.idempotencyKey, "idempotencyKey"),
1225
1284
  }));
1226
- if (operation === "node.reopen")
1227
- return unwrap(task.commands.reopenNode({
1228
- 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,
1229
1292
  nodeId: string(value.nodeId, "nodeId"),
1230
1293
  reason: string(value.reason, "reason"),
1231
1294
  expectedTaskVersion: Number(value.expectedTaskVersion),
1232
1295
  actorRef: "extension:human",
1233
1296
  idempotencyKey: string(value.idempotencyKey, "idempotencyKey"),
1234
1297
  }));
1298
+ reconciliationCoordinator?.kick(taskId);
1299
+ return result;
1300
+ }
1235
1301
  throw new Error("UNSUPPORTED_TASK_APPLICATION_OPERATION");
1236
1302
  },
1237
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
+ };
1238
1333
  const approvalApplication = Object.freeze({
1239
1334
  async invoke(operation, rawInput) {
1240
1335
  const value = object(rawInput, "approval application input");
@@ -1249,52 +1344,42 @@ async function constructGraph(config, executionCredential, modelCredential) {
1249
1344
  ...value,
1250
1345
  actorRef: "extension:human",
1251
1346
  });
1252
- if (operation === "approval.allow" || operation === "approval.deny")
1253
- 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", {
1254
1350
  contract: "execution.approval",
1255
1351
  contractVersion: "1.0.0",
1256
- approvalRef: string(value.approvalRef, "approvalRef"),
1352
+ approvalRef,
1257
1353
  actorRef: "extension:human",
1258
1354
  expectedVersion: Number(value.expectedVersion),
1259
1355
  decision: operation === "approval.allow" ? "ALLOW" : "DENY",
1260
1356
  ...(typeof value.reason === "string" ? { reason: value.reason } : {}),
1261
1357
  });
1262
- if (operation === "approval.revoke")
1263
- 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", {
1264
1364
  contract: "execution.approval",
1265
1365
  contractVersion: "1.0.0",
1266
- approvalRef: string(value.approvalRef, "approvalRef"),
1366
+ approvalRef,
1267
1367
  actorRef: "extension:human",
1268
1368
  expectedVersion: Number(value.expectedVersion),
1269
1369
  reason: string(value.reason, "reason"),
1270
1370
  });
1371
+ await reconcileApproval(approvalRef);
1372
+ return result;
1373
+ }
1271
1374
  throw new Error("UNSUPPORTED_APPROVAL_APPLICATION_OPERATION");
1272
1375
  },
1273
1376
  });
1274
1377
  const observerApplication = Object.freeze({
1275
1378
  async invoke(operation, rawInput) {
1276
1379
  const value = object(rawInput, "observer application input");
1277
- if (operation === "task.projection")
1278
- return taskDriverPorts.getTaskDriveProjection(string(value.taskId, "taskId"));
1279
- if (operation === "approval.executionContext") {
1280
- const approval = object(await execution.invoke("getExecutionApproval", {
1281
- approvalRef: string(value.approvalRef, "approvalRef"),
1282
- }), "approval fact");
1283
- const executionFact = object(await execution.invoke("getExecution", {
1284
- executionRef: string(approval.executionRef, "executionRef"),
1285
- callerRef: string(approval.callerRef, "callerRef"),
1286
- }), "execution fact");
1287
- const runNo = Number(executionFact.runNo);
1288
- if (!Number.isInteger(runNo) || runNo <= 0)
1289
- throw new Error("EXECUTION_RUN_GENERATION_REQUIRED");
1290
- return {
1291
- executionRef: string(executionFact.executionRef, "executionRef"),
1292
- taskId: string(executionFact.taskId, "taskId"),
1293
- nodeId: string(executionFact.nodeId, "nodeId"),
1294
- runNo,
1295
- roleRef: string(executionFact.roleRef, "roleRef"),
1296
- workerRef: string(executionFact.workerRef, "workerRef"),
1297
- };
1380
+ if (operation === "task.reconcileAll") {
1381
+ void reconciliationCoordinator?.sweep();
1382
+ return { scheduled: true };
1298
1383
  }
1299
1384
  if (operation === "browser.permission.classify") {
1300
1385
  const roleRef = string(value.roleRef, "roleRef");
@@ -1379,79 +1464,6 @@ async function constructGraph(config, executionCredential, modelCredential) {
1379
1464
  });
1380
1465
  return { reported: true };
1381
1466
  }
1382
- if (operation === "execution.listSignals")
1383
- return execution.invoke("listExecutionObserverSignals", {
1384
- limit: Number(value.limit ?? 50),
1385
- });
1386
- if (operation === "execution.ackSignal")
1387
- return execution.invoke("acknowledgeExecutionObserverSignal", {
1388
- signalRef: string(value.signalRef, "signalRef"),
1389
- });
1390
- if (operation === "task.wake") {
1391
- const taskId = string(value.taskId, "taskId");
1392
- const nodeId = string(value.nodeId, "nodeId");
1393
- const runNo = Number(value.runNo);
1394
- const roleRef = string(value.roleRef, "roleRef");
1395
- const workerRef = string(value.workerRef, "workerRef");
1396
- const trigger = string(value.trigger, "trigger");
1397
- const underlyingRef = typeof value.underlyingRef === "string"
1398
- ? value.underlyingRef
1399
- : "none";
1400
- const wakeExecution = object(await execution.invoke("executeCapability", {
1401
- contract: "execution",
1402
- contractVersion: "1.0.0",
1403
- idempotencyKey: `task-observer-wake:${taskId}:${nodeId}:${runNo}:${trigger}:${underlyingRef}`,
1404
- callerRef: "extension:task-observer",
1405
- correlationId: `task-observer:${taskId}:${nodeId}:${runNo}`,
1406
- taskId,
1407
- nodeId,
1408
- runNo,
1409
- roleRef,
1410
- workerRef,
1411
- capability: "worker.wake",
1412
- input: {
1413
- roleRef,
1414
- workerRef,
1415
- taskId,
1416
- nodeId,
1417
- runNo,
1418
- trigger,
1419
- fingerprint: `wake:${taskId}:${nodeId}:${runNo}:${trigger}:${underlyingRef}`,
1420
- },
1421
- }), "task wake execution");
1422
- if (wakeExecution.status !== "SUCCEEDED" ||
1423
- wakeExecution.sideEffectState !== "APPLIED")
1424
- throw new Error(`TASK_WAKE_NOT_CONFIRMED:${String(wakeExecution.status)}:${String(wakeExecution.sideEffectState)}`);
1425
- return wakeExecution;
1426
- }
1427
- if (operation === "task.diagnostic") {
1428
- const response = object(await model.invoke("infer", {
1429
- contractVersion: "1.0.0",
1430
- specRef: "task.diagnostic.v1",
1431
- mode: "reason",
1432
- priority: "business",
1433
- trace: {
1434
- callerRef: "extension:task-observer",
1435
- correlationId: string(value.correlationId, "correlationId"),
1436
- taskId: string(value.taskId, "taskId"),
1437
- nodeId: string(value.nodeId, "nodeId"),
1438
- },
1439
- payload: value.payload,
1440
- }), "task diagnostic inference");
1441
- if (response.status !== "SUCCEEDED") {
1442
- const error = typeof response.error === "object" && response.error !== null
1443
- ? response.error
1444
- : {};
1445
- const code = error.code === "CONTEXT_TOO_LARGE"
1446
- ? "CONTEXT_TOO_LARGE"
1447
- : error.code === "MODEL_UNAVAILABLE" ||
1448
- error.code === "CAPABILITY_UNSUPPORTED"
1449
- ? "REASON_UNAVAILABLE"
1450
- : "REASON_FAILED";
1451
- return { ok: false, errorCode: code };
1452
- }
1453
- return taskDiagnosticReasonResultSchema.parse(response.data);
1454
- }
1455
1467
  if (operation === "system.view")
1456
1468
  return boundedSystemView(string(value.view, "view"));
1457
1469
  if (operation === "system.drilldown") {
@@ -1504,6 +1516,7 @@ async function constructGraph(config, executionCredential, modelCredential) {
1504
1516
  throw new Error("UNSUPPORTED_OBSERVER_APPLICATION_OPERATION");
1505
1517
  },
1506
1518
  });
1519
+ reconciliationCoordinator.start();
1507
1520
  return Object.freeze({
1508
1521
  route,
1509
1522
  browserOwnerPorts,
@@ -1514,30 +1527,10 @@ async function constructGraph(config, executionCredential, modelCredential) {
1514
1527
  taskApplication,
1515
1528
  approvalApplication,
1516
1529
  observerApplication,
1517
- async lookup(operationId, authenticatedRoleRef, input) {
1518
- const value = object(input, "lookup input");
1519
- if (operationId === "executeCapability") {
1520
- if (value.executionRef) {
1521
- const record = await execution.invoke("getExecution", {
1522
- ...value,
1523
- callerRef: authenticatedRoleRef,
1524
- });
1525
- return admitExecutionRead(authenticatedRoleRef, record);
1526
- }
1527
- const scope = await admitExactNodeExecution(value, authenticatedRoleRef);
1528
- const { workerRef: _suppliedWorkerRef, projectRoot: _suppliedProjectRoot, roleRef: _suppliedRoleRef, callerRef: _suppliedCallerRef, ...ownerInput } = value;
1529
- const record = await execution.invoke("lookupExecutionIntent", {
1530
- ...ownerInput,
1531
- callerRef: authenticatedRoleRef,
1532
- roleRef: authenticatedRoleRef,
1533
- taskId: scope.taskId,
1534
- nodeId: scope.nodeId,
1535
- runNo: scope.runNo,
1536
- workerRef: scope.workerRef,
1537
- });
1538
- return admitExecutionRead(authenticatedRoleRef, record);
1539
- }
1540
- 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
+ });
1541
1534
  },
1542
1535
  async readiness() {
1543
1536
  const diagnostics = taskStore.diagnostics();
@@ -1566,6 +1559,7 @@ async function constructGraph(config, executionCredential, modelCredential) {
1566
1559
  };
1567
1560
  },
1568
1561
  close() {
1562
+ reconciliationCoordinator?.stop();
1569
1563
  agent.close();
1570
1564
  taskStore.close();
1571
1565
  },
@@ -1722,7 +1716,7 @@ export function createPlatformHost(input) {
1722
1716
  liveness: lifecycle === "STOPPED" ? "DOWN" : "UP",
1723
1717
  transport: server ? "UP" : "DOWN",
1724
1718
  readiness: accepting &&
1725
- Object.values(dependencies).every((item) => item.status === "READY")
1719
+ [dependencies.task, dependencies.agent].every((item) => item.status === "READY")
1726
1720
  ? "READY"
1727
1721
  : "NOT_READY",
1728
1722
  accepting,
@@ -1821,8 +1815,7 @@ export function createPlatformHost(input) {
1821
1815
  log("DEPENDENCY_INITIALIZATION_STARTED", {
1822
1816
  order: ["task", "agent", "execution-client", "model-client"],
1823
1817
  });
1824
- graph = await constructGraph(input.config, executionTransportCredential, modelTransportCredential);
1825
- await graph.readiness();
1818
+ graph = await constructGraph(input.config, executionTransportCredential, modelTransportCredential, input.resolveOwnerConnection, input.resolveLocalToolConnection);
1826
1819
  server = createServer((request, response) => {
1827
1820
  const work = (async () => {
1828
1821
  const url = new URL(request.url ?? "/", "http://platform-host.local");
@@ -2044,11 +2037,16 @@ export function createPlatformHost(input) {
2044
2037
  const operationId = decodeURIComponent(lookup ? suffix.slice(0, -"/result".length) : suffix);
2045
2038
  const result = lookup
2046
2039
  ? await graph.lookup(operationId, authenticatedRoleRef, body.input)
2047
- : await graph.route(operationId, authenticatedRoleRef, body.input, body.fileMaterializationInputs === undefined
2048
- ? undefined
2049
- : {
2050
- fileMaterializationInputs: body.fileMaterializationInputs,
2051
- });
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
+ });
2052
2050
  respond(response, 200, result);
2053
2051
  }
2054
2052
  catch (error) {