@tomflow/proflow-platform-host 0.1.30 → 0.1.32

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
@@ -5,7 +5,9 @@ import { chmod, mkdir, readFile, stat, writeFile, } from "node:fs/promises";
5
5
  import { createServer } from "node:http";
6
6
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
7
7
  import { createAgentRuntime } from "@tomflow/proflow-agent-runtime";
8
+ import { createMonitorControlClient, } from "@tomflow/proflow-execution-browser-extension";
8
9
  import { createLocalToolBridgeHostClient, LocalToolBridgeError, } from "@tomflow/proflow-execution-browser-extension/local-tool-bridge";
10
+ import { browserActionInputSchema, parseMonitorEffectRequest } from "@tomflow/proflow-execution-contracts";
9
11
  import { applyMigrations } from "@tomflow/proflow-task-migration-runner";
10
12
  import { createTaskServices, publicOperationNames, } from "@tomflow/proflow-task-orchestration";
11
13
  import { SqliteTaskStore } from "@tomflow/proflow-task-store-sqlite";
@@ -13,10 +15,24 @@ import { taskMigrations } from "@tomflow/proflow-task-store-sqlite/migrations";
13
15
  import { z } from "zod";
14
16
  import { resolveBrowserPermissionTaskBinding } from "./browser-permission-context.js";
15
17
  import { classifyBrowserPermission, } from "./browser-permission-policy.js";
18
+ import { authorizeMonitorExecution } from "./monitor-rotation-coordinator.js";
19
+ import { createMonitorDriveRelay } from "./monitor-drive-relay.js";
20
+ import { canonicalizeProductOperation, createProductCampaignContinuationCoordinator, } from "./product-campaign-continuation.js";
21
+ import { createProductDiscussionHost } from "./product-discussion-host.js";
16
22
  import { createReconciliationCoordinator } from "./reconciliation-coordinator.js";
17
23
  import { directToolActionIds, roleAllowsDirectToolOperation, roleOperations, rolePackageRefs, } from "./role-operations.js";
18
24
  const loopbackHosts = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
19
25
  const OWNER_INVOKE_WATCHDOG_MS = 120_000;
26
+ const productCampaignOperationIds = new Set([
27
+ "getProductCampaign",
28
+ "getProductDiscussionContext",
29
+ "getProductDocument",
30
+ "putProductDocument",
31
+ "getCampaignAuthorization",
32
+ "submitProductTaskIntent",
33
+ "recordProductGapReview",
34
+ "recordProductGoalSatisfied",
35
+ ]);
20
36
  const systemObserverReasonResultSchema = z
21
37
  .object({
22
38
  scope: z.string().min(1).max(240).optional(),
@@ -207,7 +223,7 @@ async function responseJson(response) {
207
223
  });
208
224
  return value;
209
225
  }
210
- function createOwnerHttpClient(owner, baseUrl, credential, resolveConnection) {
226
+ function createOwnerHttpClient(owner, baseUrl, credential, resolveConnection, shutdownSignal) {
211
227
  const connection = async () => {
212
228
  const resolved = resolveConnection
213
229
  ? await resolveConnection(owner)
@@ -262,6 +278,8 @@ function createOwnerHttpClient(owner, baseUrl, credential, resolveConnection) {
262
278
  }
263
279
  else if (operationId === "materializeExternalFiles")
264
280
  path = "/external-files/materialize";
281
+ else if (operationId === "effect.execute")
282
+ path = "/effects/execute";
265
283
  else if (operationId === "executeCapability")
266
284
  path = "/executions";
267
285
  else if (operationId === "lookupExecutionIntent")
@@ -319,7 +337,9 @@ function createOwnerHttpClient(owner, baseUrl, credential, resolveConnection) {
319
337
  ...(method === "POST"
320
338
  ? { body: JSON.stringify(requestBody) }
321
339
  : {}),
322
- signal: AbortSignal.timeout(OWNER_INVOKE_WATCHDOG_MS),
340
+ signal: shutdownSignal
341
+ ? AbortSignal.any([shutdownSignal, AbortSignal.timeout(operationId === "effect.execute" ? 190_000 : OWNER_INVOKE_WATCHDOG_MS)])
342
+ : AbortSignal.timeout(operationId === "effect.execute" ? 190_000 : OWNER_INVOKE_WATCHDOG_MS),
323
343
  }));
324
344
  },
325
345
  });
@@ -382,7 +402,7 @@ async function readDeploymentOwnerSummary(stateRoot) {
382
402
  return undefined;
383
403
  }
384
404
  }
385
- async function constructGraph(config, executionCredential, modelCredential, resolveConnection, resolveLocalToolConnection) {
405
+ async function constructGraph(config, executionCredential, modelCredential, resolveConnection, resolveLocalToolConnection, resolveMonitorControlConnection) {
386
406
  const sink = createOperationSink(config.stateRoot);
387
407
  const observer = createHostOperationObserver(entry => sink.write("platform-host", entry));
388
408
  const databasePath = join(config.stateRoot, "state", "task.sqlite");
@@ -445,9 +465,27 @@ async function constructGraph(config, executionCredential, modelCredential, reso
445
465
  taskStore.close();
446
466
  throw error;
447
467
  }
448
- const execution = observer.port("execution", createOwnerHttpClient("execution", config.executionBaseUrl, executionCredential, resolveConnection));
449
- const model = observer.port("model", createOwnerHttpClient("model", config.modelBaseUrl, modelCredential, resolveConnection));
468
+ const ownerShutdown = new AbortController();
469
+ const execution = observer.port("execution", createOwnerHttpClient("execution", config.executionBaseUrl, executionCredential, resolveConnection, ownerShutdown.signal));
470
+ const model = observer.port("model", createOwnerHttpClient("model", config.modelBaseUrl, modelCredential, resolveConnection, ownerShutdown.signal));
471
+ const monitor = resolveMonitorControlConnection
472
+ ? Object.freeze({
473
+ async invoke(operation, input = {}) {
474
+ const connection = await resolveMonitorControlConnection();
475
+ if (!connection)
476
+ throw new Error("MONITOR_CONTROL_UNAVAILABLE");
477
+ return createMonitorControlClient({
478
+ endpoint: connection.endpoint,
479
+ token: connection.credential,
480
+ }).invoke(operation, input);
481
+ },
482
+ })
483
+ : undefined;
484
+ const monitorDriveRelay = monitor
485
+ ? createMonitorDriveRelay({ monitor, execution })
486
+ : undefined;
450
487
  let reconciliationCoordinator;
488
+ let productCampaignContinuationCoordinator;
451
489
  const boundedSystemView = async (view) => {
452
490
  if (view === "task") {
453
491
  const tasks = unwrap(task.queries.listTasks({})).tasks;
@@ -649,8 +687,59 @@ async function constructGraph(config, executionCredential, modelCredential, reso
649
687
  throw Object.assign(new Error(code), { httpStatus });
650
688
  }
651
689
  }
652
- const taskOperation = taskOperations.get(operationId);
690
+ const taskOperation = operationId === "getProductDiscussionContext"
691
+ ? () => {
692
+ const session = agent.getProductDiscussionSession(string(input.discussionRef, "discussionRef"));
693
+ return { ok: true, data: { ...session, discussionGeneration: session.generation } };
694
+ }
695
+ : taskOperations.get(operationId);
653
696
  if (taskOperation) {
697
+ const productOperation = role.agentPackageRef === "@tomflow/proflow-agent-product" &&
698
+ productCampaignOperationIds.has(operationId)
699
+ ? canonicalizeProductOperation({
700
+ operationId,
701
+ authenticatedRoleRef,
702
+ rawInput: input,
703
+ getDiscussionSession: (discussionRef) => agent.getProductDiscussionSession(discussionRef),
704
+ getRoleBindings: () => rolePackageRefs.map((agentPackageRef) => ({
705
+ agentPackageRef,
706
+ roleRef: roleForPackage(agentPackageRef).roleRef,
707
+ })),
708
+ verifyDiscussionAdmission: (admission) => {
709
+ if (!productDiscussionHost?.verifyAdmission(admission))
710
+ return false;
711
+ return !unwrap(task.queries.listTasks({})).tasks.some((summary) => unwrap(task.queries.getTask({ taskId: summary.taskId })).roleBindings.some((binding) => binding.conversationLocator === admission.conversationLocator ||
712
+ binding.workerRef === admission.conversationRef));
713
+ },
714
+ })
715
+ : undefined;
716
+ if (productOperation?.scope === "PRODUCT_QUERY") {
717
+ if (operationId === "getCampaignAuthorization") {
718
+ const authorization = unwrap(task.queries.getCampaignAuthorization(productOperation.input));
719
+ if (authorization.campaignRef !== input.campaignRef ||
720
+ authorization.discussionRef !== input.discussionRef)
721
+ throw Object.assign(new Error("PRODUCT_DISCUSSION_CAMPAIGN_MISMATCH"), { httpStatus: 403 });
722
+ }
723
+ return taskOperation(productOperation.input);
724
+ }
725
+ if (productOperation?.scope === "PRODUCT_MUTATION") {
726
+ const taskResult = await taskOperation({
727
+ ...productOperation.input,
728
+ actorRef: productOperation.actorRef,
729
+ });
730
+ if (productOperation.kickContinuation &&
731
+ typeof taskResult === "object" &&
732
+ taskResult !== null &&
733
+ Reflect.get(taskResult, "ok") === true) {
734
+ const data = Reflect.get(taskResult, "data");
735
+ if (typeof data === "object" && data !== null) {
736
+ const taskId = Reflect.get(data, "taskId");
737
+ if (typeof taskId === "string")
738
+ productCampaignContinuationCoordinator?.kick(taskId);
739
+ }
740
+ }
741
+ return taskResult;
742
+ }
654
743
  let actorRef = authenticatedRoleRef;
655
744
  let canonicalWorkerRef;
656
745
  if (typeof input.taskId === "string") {
@@ -691,8 +780,10 @@ async function constructGraph(config, executionCredential, modelCredential, reso
691
780
  const taskResult = await taskOperation(queryOperations.has(operationId)
692
781
  ? canonicalTaskInput
693
782
  : { ...canonicalTaskInput, actorRef });
694
- if (taskMutationOperations.has(operationId) && typeof input.taskId === "string")
783
+ if (taskMutationOperations.has(operationId) && typeof input.taskId === "string") {
695
784
  reconciliationCoordinator?.kick(input.taskId);
785
+ productCampaignContinuationCoordinator?.kick(input.taskId);
786
+ }
696
787
  if (operationId === "getTaskDocument")
697
788
  return fileBridgeOutputForTaskResult(taskResult);
698
789
  return taskResult;
@@ -713,8 +804,14 @@ async function constructGraph(config, executionCredential, modelCredential, reso
713
804
  const declared = current.roleBindings.find((item) => item.roleRef === binding.roleRef);
714
805
  if (!declared)
715
806
  throw new Error("AGENT_PACKAGE_NOT_ELIGIBLE");
716
- if (declared.workerRef === binding.workerRef)
807
+ if (declared.workerRef === binding.workerRef) {
808
+ if (declared.conversationLocator !== null &&
809
+ declared.conversationLocator !== binding.conversationLocator)
810
+ throw new Error("TASK_ROLE_BINDING_CONVERSATION_MISMATCH");
811
+ if (declared.conversationLocator === binding.conversationLocator)
812
+ productDiscussionHost?.releaseTaskWorker(binding.taskId, binding.roleRef);
717
813
  return;
814
+ }
718
815
  if (declared.workerRef)
719
816
  throw new Error("TASK_ROLE_BINDING_CONFLICT");
720
817
  unwrap(task.commands.bindTaskWorker({
@@ -727,7 +824,9 @@ async function constructGraph(config, executionCredential, modelCredential, reso
727
824
  actorRef: "platform-host:worker-provisioning",
728
825
  idempotencyKey: `browser-bind:${binding.taskId}:${binding.roleRef}:${binding.workerRef}`,
729
826
  }));
827
+ productDiscussionHost?.releaseTaskWorker(binding.taskId, binding.roleRef);
730
828
  reconciliationCoordinator?.kick(binding.taskId);
829
+ productCampaignContinuationCoordinator?.kick(binding.taskId);
731
830
  },
732
831
  }),
733
832
  agent: Object.freeze({
@@ -764,6 +863,94 @@ async function constructGraph(config, executionCredential, modelCredential, reso
764
863
  });
765
864
  const authorizeExecution = async (request) => {
766
865
  try {
866
+ if (request.capability === "browser.action") {
867
+ if (request.callerRef !== "platform-host:browser-action")
868
+ return false;
869
+ const input = browserActionInputSchema.parse(request.input);
870
+ // Authenticated local Action owner; reload must also work before Monitor initialization.
871
+ if (input.kind === "RELOAD")
872
+ return true;
873
+ if (!monitor)
874
+ return false;
875
+ const state = object(await monitor.invoke("state.read", {}), "monitor state");
876
+ const chats = object(state.chats, "monitor chats");
877
+ const config = object(state.config, "monitor config");
878
+ const allowed = new Set(Object.values(chats).flatMap((chat) => {
879
+ if (!chat || typeof chat !== "object" || !("conversationLocator" in chat) ||
880
+ typeof chat.conversationLocator !== "string")
881
+ return [];
882
+ return [chat.conversationLocator];
883
+ }));
884
+ if (typeof config.projectLocator === "string")
885
+ allowed.add(config.projectLocator);
886
+ allowed.add("chrome://extensions/?id=eehdadpmjffomabiedcjijiakconalab");
887
+ allowed.add("chrome://extensions/?errors=eehdadpmjffomabiedcjijiakconalab");
888
+ return input.targets.every((target) => allowed.has(target.url));
889
+ }
890
+ if (request.capability === "product.discussion.bootstrap.deliver")
891
+ return productDiscussionHost
892
+ ? productDiscussionHost.authorizeBootstrapExecution(request)
893
+ : false;
894
+ if (request.capability === "monitor.chat.submit" ||
895
+ request.capability === "monitor.chat.create") {
896
+ if (!monitor)
897
+ return false;
898
+ if (request.callerRef === "platform-host:monitor-drive") {
899
+ const authorization = object(await monitor.invoke("drive.authorize", { request }), "monitor drive authorization");
900
+ return authorization.authorized === true;
901
+ }
902
+ // Transitional compatibility until legacy Monitor lifecycle code is removed.
903
+ return authorizeMonitorExecution({
904
+ monitor,
905
+ request: request,
906
+ });
907
+ }
908
+ if (request.capability === "product.review.deliver") {
909
+ if (request.callerRef !== "platform-host:campaign-continuation")
910
+ return false;
911
+ if (request.taskId !== undefined ||
912
+ request.nodeId !== undefined ||
913
+ request.runNo !== undefined ||
914
+ request.workerRef !== undefined ||
915
+ request.projectRoot !== undefined)
916
+ return false;
917
+ const reviewInput = object(request.input, "product review execution input");
918
+ const reviewRef = string(reviewInput.reviewRef, "input.reviewRef");
919
+ const discussionRef = string(reviewInput.discussionRef, "input.discussionRef");
920
+ const discussionGeneration = positiveInteger(reviewInput.discussionGeneration, "input.discussionGeneration");
921
+ const roleRef = string(reviewInput.roleRef, "input.roleRef");
922
+ const conversationRef = string(reviewInput.conversationRef, "input.conversationRef");
923
+ const conversationLocator = string(reviewInput.conversationLocator, "input.conversationLocator");
924
+ const campaignRef = string(reviewInput.campaignRef, "input.campaignRef");
925
+ const intentRef = string(reviewInput.intentRef, "input.intentRef");
926
+ const terminalTaskId = string(reviewInput.terminalTaskId, "input.terminalTaskId");
927
+ const terminalTaskVersion = positiveInteger(reviewInput.terminalTaskVersion, "input.terminalTaskVersion");
928
+ if (request.roleRef !== roleRef)
929
+ return false;
930
+ const productRole = agent
931
+ .listRegisteredRoles()
932
+ .find((candidate) => candidate.agentPackageRef === "@tomflow/proflow-agent-product");
933
+ if (!productRole || productRole.roleRef !== roleRef)
934
+ return false;
935
+ const session = agent.getProductDiscussionSession(discussionRef);
936
+ if (session.status !== "ACTIVE" ||
937
+ session.roleRef !== roleRef ||
938
+ session.generation !== discussionGeneration ||
939
+ session.conversationRef !== conversationRef ||
940
+ session.conversationLocator !== conversationLocator ||
941
+ session.campaignRef !== campaignRef ||
942
+ session.currentIntentRef !== intentRef)
943
+ return false;
944
+ const terminalTask = unwrap(task.queries.getTask({ taskId: terminalTaskId }));
945
+ if (!taskIsTerminal(terminalTask.status) ||
946
+ terminalTask.version !== terminalTaskVersion)
947
+ return false;
948
+ const expectedReviewRef = `product-review:${campaignRef}:${terminalTaskId}:${terminalTaskVersion}:${discussionRef}:${discussionGeneration}`;
949
+ if (reviewRef !== expectedReviewRef ||
950
+ reviewInput.contentFingerprint !== expectedReviewRef)
951
+ return false;
952
+ return true;
953
+ }
767
954
  const browserCapability = request.capability === "worker.create" ||
768
955
  request.capability === "worker.restore" ||
769
956
  request.capability === "worker.wake" ||
@@ -908,6 +1095,29 @@ async function constructGraph(config, executionCredential, modelCredential, reso
908
1095
  throw new Error("ROLE_NOT_FOUND");
909
1096
  return role;
910
1097
  };
1098
+ const productRole = agent
1099
+ .listRegisteredRoles()
1100
+ .find((candidate) => candidate.agentPackageRef === "@tomflow/proflow-agent-product");
1101
+ const productDiscussionHost = productRole
1102
+ ? createProductDiscussionHost({
1103
+ productRoleRef: productRole.roleRef,
1104
+ listTaskIds: () => unwrap(task.queries.listTasks({})).tasks.map((summary) => summary.taskId),
1105
+ getTaskFact: (taskId) => {
1106
+ const current = unwrap(task.queries.getTask({ taskId }));
1107
+ return {
1108
+ taskId: current.taskId,
1109
+ status: current.status,
1110
+ roleBindings: current.roleBindings,
1111
+ };
1112
+ },
1113
+ registerDiscussionSession: (input) => agent.registerProductDiscussionSession(input),
1114
+ getDiscussionSession: (discussionRef) => agent.getProductDiscussionSession(discussionRef),
1115
+ async executeCapability(request) {
1116
+ return execution.invoke("executeCapability", request);
1117
+ },
1118
+ })
1119
+ : undefined;
1120
+ await productDiscussionHost?.recoverReservations();
911
1121
  const browserPermissionRole = (roleRef) => {
912
1122
  const role = agent.listRegisteredRoles().find((candidate) => candidate.roleRef === roleRef);
913
1123
  if (!role || !rolePackageRefs.includes(role.agentPackageRef))
@@ -953,6 +1163,49 @@ async function constructGraph(config, executionCredential, modelCredential, reso
953
1163
  }
954
1164
  if (operation === "role.list")
955
1165
  return agent.listRegisteredRoles();
1166
+ if (operation === "product.campaign.authorize") {
1167
+ const discussionRef = string(value.discussionRef, "discussionRef");
1168
+ const campaignRef = string(value.campaignRef, "campaignRef");
1169
+ const goalRevision = positiveInteger(value.goalRevision, "goalRevision");
1170
+ const productRole = roleForPackage("@tomflow/proflow-agent-product");
1171
+ const before = agent.getProductDiscussionSession(discussionRef);
1172
+ if (before.status !== "ACTIVE" || before.roleRef !== productRole.roleRef)
1173
+ throw new Error("PRODUCT_DISCUSSION_SESSION_NOT_READY");
1174
+ const granted = unwrap(task.commands.createCampaignAuthorization({
1175
+ ...(value.authorizationRef === undefined
1176
+ ? {}
1177
+ : { authorizationRef: string(value.authorizationRef, "authorizationRef") }),
1178
+ campaignRef,
1179
+ discussionRef,
1180
+ goalRevision,
1181
+ goalSummary: string(value.goalSummary, "goalSummary"),
1182
+ scope: value.scope,
1183
+ constraints: value.constraints,
1184
+ actorRef: "trusted:product-control",
1185
+ idempotencyKey: string(value.idempotencyKey, "idempotencyKey"),
1186
+ }));
1187
+ const current = agent.getProductDiscussionSession(discussionRef);
1188
+ if (current.status !== "ACTIVE" || current.roleRef !== productRole.roleRef)
1189
+ throw new Error("PRODUCT_DISCUSSION_SESSION_NOT_READY");
1190
+ const discussionSession = current.campaignRef === campaignRef &&
1191
+ current.goalRevision === goalRevision
1192
+ ? current
1193
+ : agent.updateProductDiscussionSessionContext({
1194
+ discussionRef,
1195
+ expectedVersion: current.version,
1196
+ campaignRef,
1197
+ goalRevision,
1198
+ });
1199
+ return { ...granted, discussionSession };
1200
+ }
1201
+ if (operation === "product.campaign.revoke")
1202
+ return unwrap(task.commands.revokeCampaignAuthorization({
1203
+ authorizationRef: string(value.authorizationRef, "authorizationRef"),
1204
+ expectedAuthorizationVersion: positiveInteger(value.expectedAuthorizationVersion, "expectedAuthorizationVersion"),
1205
+ reason: string(value.reason, "reason"),
1206
+ actorRef: "trusted:product-control",
1207
+ idempotencyKey: string(value.idempotencyKey, "idempotencyKey"),
1208
+ }));
956
1209
  const agentPackageRef = string(value.agentPackageRef, "agentPackageRef");
957
1210
  const role = roleForPackage(agentPackageRef);
958
1211
  if (operation === "role.show")
@@ -989,9 +1242,12 @@ async function constructGraph(config, executionCredential, modelCredential, reso
989
1242
  const binding = current.roleBindings.find((candidate) => candidate.agentPackageRef === agentPackageRef);
990
1243
  if (!binding)
991
1244
  throw new Error("TASK_ROLE_BINDING_REQUIRED");
992
- if (binding.workerRef && binding.conversationLocator)
1245
+ if (binding.workerRef && binding.conversationLocator) {
1246
+ productDiscussionHost?.releaseTaskWorker(taskId, binding.roleRef);
993
1247
  return;
1248
+ }
994
1249
  const role = roleForPackage(agentPackageRef);
1250
+ productDiscussionHost?.reserveTaskWorker(taskId, role.roleRef);
995
1251
  const executionRecord = object(await execution.invoke("executeCapability", {
996
1252
  contract: "execution",
997
1253
  contractVersion: "1.0.0",
@@ -1007,12 +1263,18 @@ async function constructGraph(config, executionCredential, modelCredential, reso
1007
1263
  bootstrapFingerprint: `new-task:${taskId}:${agentPackageRef}`,
1008
1264
  },
1009
1265
  }), "worker create execution");
1010
- if (executionRecord.status !== "SUCCEEDED")
1266
+ if (executionRecord.status !== "SUCCEEDED") {
1267
+ current = unwrap(task.queries.getTask({ taskId }));
1268
+ const observed = current.roleBindings.find((candidate) => candidate.agentPackageRef === agentPackageRef);
1269
+ if (observed?.workerRef && observed.conversationLocator)
1270
+ productDiscussionHost?.releaseTaskWorker(taskId, observed.roleRef);
1011
1271
  throw new Error(`WORKER_CREATE_NOT_CONFIRMED:${String(executionRecord.status)}`);
1272
+ }
1012
1273
  current = unwrap(task.queries.getTask({ taskId }));
1013
1274
  const persisted = current.roleBindings.find((candidate) => candidate.agentPackageRef === agentPackageRef);
1014
1275
  if (!persisted?.workerRef || !persisted.conversationLocator)
1015
1276
  throw new Error("WORKER_CREATE_BINDING_NOT_PERSISTED");
1277
+ productDiscussionHost?.releaseTaskWorker(taskId, persisted.roleRef);
1016
1278
  };
1017
1279
  const waitFor = new Set(options?.waitFor ?? []);
1018
1280
  const shouldWait = (agentPackageRef) => waitFor.size === 0 || waitFor.has(roleForPackage(agentPackageRef).roleRef);
@@ -1081,6 +1343,180 @@ async function constructGraph(config, executionCredential, modelCredential, reso
1081
1343
  getProjection: (taskId) => taskDriverPorts.getTaskDriveProjection(taskId),
1082
1344
  requestWake: requestTaskWake,
1083
1345
  });
1346
+ productCampaignContinuationCoordinator = createProductCampaignContinuationCoordinator({
1347
+ async getProjection(taskId) {
1348
+ const result = task.queries.getProductContinuationProjection({ taskId });
1349
+ if (!result.ok) {
1350
+ const code = typeof result.error === "object" && result.error !== null
1351
+ ? Reflect.get(result.error, "code")
1352
+ : undefined;
1353
+ if (code === "PRODUCT_CAMPAIGN_NOT_FOUND")
1354
+ return null;
1355
+ throw new Error(`PRODUCT_CONTINUATION_PROJECTION_FAILED:${String(code ?? "UNKNOWN")}`);
1356
+ }
1357
+ return result.data;
1358
+ },
1359
+ async listTaskIds() {
1360
+ return unwrap(task.queries.listTasks({})).tasks.map((summary) => summary.taskId);
1361
+ },
1362
+ async ensureWorkers(taskId) {
1363
+ await ensureTaskWorkers(taskId);
1364
+ },
1365
+ async transferRequirement(input) {
1366
+ unwrap(task.commands.transferProductIntentToTask({
1367
+ ...input,
1368
+ actorRef: "platform:campaign-continuation",
1369
+ }));
1370
+ },
1371
+ async startTask(input) {
1372
+ unwrap(task.commands.startTask({
1373
+ taskId: input.taskId,
1374
+ expectedTaskVersion: input.expectedTaskVersion,
1375
+ startAuthority: {
1376
+ type: "CAMPAIGN_AUTHORIZATION",
1377
+ authorizationRef: input.authorizationRef,
1378
+ expectedAuthorizationVersion: input.expectedAuthorizationVersion,
1379
+ expectedCampaignVersion: input.expectedCampaignVersion,
1380
+ },
1381
+ actorRef: "platform:campaign-continuation",
1382
+ idempotencyKey: input.idempotencyKey,
1383
+ }));
1384
+ },
1385
+ getDiscussionSessionForCampaign: (campaignRef) => agent.getProductDiscussionSessionForCampaign(campaignRef),
1386
+ updateDiscussionSessionContext: (input) => agent.updateProductDiscussionSessionContext(input),
1387
+ kickTaskReconciliation: (taskId) => reconciliationCoordinator?.kick(taskId),
1388
+ async onProductReviewRequired(signal) {
1389
+ const session = agent.getProductDiscussionSession(signal.discussionRef);
1390
+ if (session.status !== "ACTIVE" ||
1391
+ session.roleRef !== signal.roleRef ||
1392
+ session.generation !== signal.discussionGeneration ||
1393
+ session.conversationRef !== signal.conversationRef ||
1394
+ session.conversationLocator !== signal.conversationLocator ||
1395
+ session.campaignRef !== signal.campaignRef ||
1396
+ session.currentIntentRef !== signal.intentRef)
1397
+ throw new Error("PRODUCT_REVIEW_DISCUSSION_STALE");
1398
+ const reviewRequest = {
1399
+ contract: "execution",
1400
+ contractVersion: "1.0.0",
1401
+ idempotencyKey: signal.reviewRef,
1402
+ callerRef: "platform-host:campaign-continuation",
1403
+ correlationId: signal.reviewRef,
1404
+ roleRef: signal.roleRef,
1405
+ capability: "product.review.deliver",
1406
+ input: {
1407
+ reviewRef: signal.reviewRef,
1408
+ discussionRef: signal.discussionRef,
1409
+ discussionGeneration: signal.discussionGeneration,
1410
+ roleRef: signal.roleRef,
1411
+ conversationRef: signal.conversationRef,
1412
+ conversationLocator: signal.conversationLocator,
1413
+ campaignRef: signal.campaignRef,
1414
+ intentRef: signal.intentRef,
1415
+ terminalTaskId: signal.taskId,
1416
+ terminalTaskVersion: signal.terminalTaskVersion,
1417
+ contentFingerprint: signal.reviewRef,
1418
+ },
1419
+ };
1420
+ if (!(await authorizeExecution(reviewRequest)))
1421
+ throw new Error("PRODUCT_REVIEW_EXECUTION_IDENTITY_DENIED");
1422
+ let executionRecord;
1423
+ try {
1424
+ executionRecord = object(await execution.invoke("executeCapability", reviewRequest), "product review delivery execution");
1425
+ }
1426
+ catch (error) {
1427
+ await sink.write("platform-host", {
1428
+ timestamp: new Date().toISOString(),
1429
+ level: "WARN",
1430
+ component: "platform-host-product-campaign-continuation",
1431
+ event: "PRODUCT_REVIEW_REQUIRED",
1432
+ eventId: signal.reviewRef,
1433
+ correlationId: signal.reviewRef,
1434
+ status: "UNKNOWN",
1435
+ taskId: signal.taskId,
1436
+ roleRef: signal.roleRef,
1437
+ conversationLocator: signal.conversationLocator,
1438
+ campaignRef: signal.campaignRef,
1439
+ intentRef: signal.intentRef,
1440
+ discussionRef: signal.discussionRef,
1441
+ discussionGeneration: signal.discussionGeneration,
1442
+ terminalTaskVersion: signal.terminalTaskVersion,
1443
+ errorCode: "EXECUTION_OWNER_RESPONSE_UNKNOWN",
1444
+ });
1445
+ throw error;
1446
+ }
1447
+ const status = string(executionRecord.status, "execution.status");
1448
+ const sideEffectState = string(executionRecord.sideEffectState, "execution.sideEffectState");
1449
+ const executionRef = typeof executionRecord.executionRef === "string"
1450
+ ? executionRecord.executionRef
1451
+ : undefined;
1452
+ const executionResult = typeof executionRecord.result === "object" &&
1453
+ executionRecord.result !== null &&
1454
+ !Array.isArray(executionRecord.result)
1455
+ ? executionRecord.result
1456
+ : undefined;
1457
+ const resultData = executionResult &&
1458
+ typeof executionResult.data === "object" &&
1459
+ executionResult.data !== null &&
1460
+ !Array.isArray(executionResult.data)
1461
+ ? executionResult.data
1462
+ : undefined;
1463
+ const evidenceRef = executionResult?.capability === "product.review.deliver" &&
1464
+ resultData?.reviewRef === signal.reviewRef &&
1465
+ resultData.delivered === true &&
1466
+ typeof resultData.evidenceRef === "string"
1467
+ ? resultData.evidenceRef
1468
+ : undefined;
1469
+ const delivered = status === "SUCCEEDED" &&
1470
+ sideEffectState === "APPLIED" &&
1471
+ evidenceRef !== undefined;
1472
+ const error = typeof executionRecord.error === "object" &&
1473
+ executionRecord.error !== null &&
1474
+ !Array.isArray(executionRecord.error)
1475
+ ? executionRecord.error
1476
+ : undefined;
1477
+ const errorCode = typeof error?.code === "string" ? error.code : undefined;
1478
+ await sink.write("platform-host", {
1479
+ timestamp: new Date().toISOString(),
1480
+ level: delivered ? "INFO" : "WARN",
1481
+ component: "platform-host-product-campaign-continuation",
1482
+ event: "PRODUCT_REVIEW_REQUIRED",
1483
+ eventId: signal.reviewRef,
1484
+ correlationId: signal.reviewRef,
1485
+ status: delivered
1486
+ ? "DELIVERED"
1487
+ : status === "UNKNOWN"
1488
+ ? "UNKNOWN"
1489
+ : "FAILED",
1490
+ taskId: signal.taskId,
1491
+ roleRef: signal.roleRef,
1492
+ conversationLocator: signal.conversationLocator,
1493
+ campaignRef: signal.campaignRef,
1494
+ intentRef: signal.intentRef,
1495
+ discussionRef: signal.discussionRef,
1496
+ discussionGeneration: signal.discussionGeneration,
1497
+ terminalTaskVersion: signal.terminalTaskVersion,
1498
+ ...(executionRef ? { executionRef } : {}),
1499
+ ...(evidenceRef ? { evidenceRef } : {}),
1500
+ ...(errorCode ? { errorCode } : {}),
1501
+ });
1502
+ if (!delivered)
1503
+ throw new Error(`PRODUCT_REVIEW_DELIVERY_NOT_CONFIRMED:${status}:${sideEffectState}`);
1504
+ },
1505
+ });
1506
+ const monitorApplication = observer.port("monitor", {
1507
+ async invoke(operation, rawInput) {
1508
+ object(rawInput, "monitor application input");
1509
+ if (operation === "effect.execute") {
1510
+ parseMonitorEffectRequest(rawInput);
1511
+ return execution.invoke("effect.execute", rawInput);
1512
+ }
1513
+ if (operation !== "drive")
1514
+ throw new Error("UNSUPPORTED_MONITOR_APPLICATION_OPERATION");
1515
+ if (!monitorDriveRelay)
1516
+ throw new Error("MONITOR_CONTROL_UNAVAILABLE");
1517
+ return monitorDriveRelay.drive();
1518
+ },
1519
+ });
1084
1520
  const taskApplication = observer.port("task", {
1085
1521
  async invoke(operation, rawInput) {
1086
1522
  const value = object(rawInput, "task application input");
@@ -1243,6 +1679,18 @@ async function constructGraph(config, executionCredential, modelCredential, reso
1243
1679
  void reconciliationCoordinator?.sweep();
1244
1680
  return { scheduled: true };
1245
1681
  }
1682
+ if (operation === "product.discussion.register")
1683
+ throw new Error("PRODUCT_DISCUSSION_REGISTER_DIRECT_FORBIDDEN");
1684
+ if (operation === "product.discussion.observe") {
1685
+ if (!productDiscussionHost)
1686
+ throw new Error("PRODUCT_DISCUSSION_ADMISSION_UNAVAILABLE");
1687
+ return productDiscussionHost.observe({
1688
+ conversationLocator: string(value.conversationLocator, "conversationLocator"),
1689
+ contentInstanceId: string(value.contentInstanceId, "contentInstanceId"),
1690
+ });
1691
+ }
1692
+ if (operation === "product.discussion.get")
1693
+ return agent.getProductDiscussionSession(string(value.discussionRef, "discussionRef"));
1246
1694
  if (operation === "browser.permission.classify") {
1247
1695
  const roleRef = string(value.roleRef, "roleRef");
1248
1696
  if (value.taskId !== undefined && typeof value.taskId !== "string")
@@ -1374,6 +1822,7 @@ async function constructGraph(config, executionCredential, modelCredential, reso
1374
1822
  },
1375
1823
  });
1376
1824
  reconciliationCoordinator.start();
1825
+ productCampaignContinuationCoordinator.start();
1377
1826
  return Object.freeze({
1378
1827
  route,
1379
1828
  operationSink: sink,
@@ -1385,6 +1834,7 @@ async function constructGraph(config, executionCredential, modelCredential, reso
1385
1834
  taskApplication,
1386
1835
  approvalApplication,
1387
1836
  observerApplication,
1837
+ monitorApplication,
1388
1838
  async lookup(_operationId, _authenticatedRoleRef, _input) {
1389
1839
  throw Object.assign(new Error("ACTION_RESULT_LOOKUP_UNSUPPORTED"), { httpStatus: 409 });
1390
1840
  },
@@ -1414,8 +1864,16 @@ async function constructGraph(config, executionCredential, modelCredential, reso
1414
1864
  model: modelStatus,
1415
1865
  };
1416
1866
  },
1417
- close() {
1867
+ async close() {
1868
+ productCampaignContinuationCoordinator?.stop();
1418
1869
  reconciliationCoordinator?.stop();
1870
+ ownerShutdown.abort();
1871
+ await Promise.all([
1872
+ productCampaignContinuationCoordinator?.drain(),
1873
+ reconciliationCoordinator?.drain(),
1874
+ ]);
1875
+ productDiscussionHost?.clear();
1876
+ await sink.drain();
1419
1877
  agent.close();
1420
1878
  taskStore.close();
1421
1879
  },
@@ -1659,9 +2117,9 @@ export function createPlatformHost(input) {
1659
2117
  ? await readExecutionTransportCredential(input.config.executionTransportCredentialFile)
1660
2118
  : input.executionCredential;
1661
2119
  log("DEPENDENCY_INITIALIZATION_STARTED", {
1662
- order: ["task", "agent", "execution-client", "model-client"],
2120
+ order: ["task", "agent", "execution-client", "model-client", "monitor-control-client"],
1663
2121
  });
1664
- graph = await constructGraph(input.config, executionTransportCredential, modelTransportCredential, input.resolveOwnerConnection, input.resolveLocalToolConnection);
2122
+ graph = await constructGraph(input.config, executionTransportCredential, modelTransportCredential, input.resolveOwnerConnection, input.resolveLocalToolConnection, input.resolveMonitorControlConnection);
1665
2123
  server = createServer((request, response) => {
1666
2124
  const work = operationContext.run({ operationRef: incomingOperationRef(request.headers["x-proflow-operation-ref"]) }, async () => {
1667
2125
  const url = new URL(request.url ?? "/", "http://platform-host.local");
@@ -1745,6 +2203,36 @@ export function createPlatformHost(input) {
1745
2203
  });
1746
2204
  }
1747
2205
  }
2206
+ if (request.method === "POST" && url.pathname === "/application/monitor") {
2207
+ if (!taskApplicationCredential ||
2208
+ !managementCredentialMatches(request.headers.authorization, taskApplicationCredential))
2209
+ return respond(response, 401, {
2210
+ error: "MONITOR_APPLICATION_AUTH_FAILED",
2211
+ });
2212
+ const chunks = [];
2213
+ let bytes = 0;
2214
+ for await (const chunk of request) {
2215
+ const buffer = Buffer.isBuffer(chunk)
2216
+ ? chunk
2217
+ : Buffer.from(chunk);
2218
+ bytes += buffer.byteLength;
2219
+ if (bytes > 65_536)
2220
+ throw new TypeError("REQUEST_BODY_TOO_LARGE");
2221
+ chunks.push(buffer);
2222
+ }
2223
+ const body = object(JSON.parse(Buffer.concat(chunks).toString("utf8")), "monitor application request");
2224
+ try {
2225
+ const result = await graph.monitorApplication.invoke(string(body.operation, "operation"), body.input ?? {});
2226
+ return respond(response, 200, result);
2227
+ }
2228
+ catch (error) {
2229
+ return respond(response, 400, {
2230
+ error: error instanceof Error
2231
+ ? error.message
2232
+ : "INVALID_REQUEST",
2233
+ });
2234
+ }
2235
+ }
1748
2236
  if (request.method === "POST" && url.pathname === "/application/observer") {
1749
2237
  if (!taskApplicationCredential ||
1750
2238
  !managementCredentialMatches(request.headers.authorization, taskApplicationCredential))
@@ -1882,7 +2370,7 @@ export function createPlatformHost(input) {
1882
2370
  accepting = false;
1883
2371
  server?.close();
1884
2372
  server = undefined;
1885
- graph?.close();
2373
+ await graph?.close();
1886
2374
  graph = undefined;
1887
2375
  roleManagementCredential = undefined;
1888
2376
  taskApplicationCredential = undefined;
@@ -1902,7 +2390,7 @@ export function createPlatformHost(input) {
1902
2390
  if (running)
1903
2391
  await new Promise((resolveStop, reject) => running.close((error) => (error ? reject(error) : resolveStop())));
1904
2392
  await Promise.allSettled([...active]);
1905
- graph?.close();
2393
+ await graph?.close();
1906
2394
  graph = undefined;
1907
2395
  roleManagementCredential = undefined;
1908
2396
  taskApplicationCredential = undefined;