@tomflow/proflow-platform-host 0.1.30 → 0.1.31

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,6 +5,7 @@ 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";
9
10
  import { applyMigrations } from "@tomflow/proflow-task-migration-runner";
10
11
  import { createTaskServices, publicOperationNames, } from "@tomflow/proflow-task-orchestration";
@@ -13,10 +14,23 @@ import { taskMigrations } from "@tomflow/proflow-task-store-sqlite/migrations";
13
14
  import { z } from "zod";
14
15
  import { resolveBrowserPermissionTaskBinding } from "./browser-permission-context.js";
15
16
  import { classifyBrowserPermission, } from "./browser-permission-policy.js";
17
+ import { authorizeMonitorExecution, createMonitorRotationCoordinator, } from "./monitor-rotation-coordinator.js";
18
+ import { canonicalizeProductOperation, createProductCampaignContinuationCoordinator, } from "./product-campaign-continuation.js";
19
+ import { createProductDiscussionHost } from "./product-discussion-host.js";
16
20
  import { createReconciliationCoordinator } from "./reconciliation-coordinator.js";
17
21
  import { directToolActionIds, roleAllowsDirectToolOperation, roleOperations, rolePackageRefs, } from "./role-operations.js";
18
22
  const loopbackHosts = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
19
23
  const OWNER_INVOKE_WATCHDOG_MS = 120_000;
24
+ const productCampaignOperationIds = new Set([
25
+ "getProductCampaign",
26
+ "getProductDiscussionContext",
27
+ "getProductDocument",
28
+ "putProductDocument",
29
+ "getCampaignAuthorization",
30
+ "submitProductTaskIntent",
31
+ "recordProductGapReview",
32
+ "recordProductGoalSatisfied",
33
+ ]);
20
34
  const systemObserverReasonResultSchema = z
21
35
  .object({
22
36
  scope: z.string().min(1).max(240).optional(),
@@ -207,7 +221,7 @@ async function responseJson(response) {
207
221
  });
208
222
  return value;
209
223
  }
210
- function createOwnerHttpClient(owner, baseUrl, credential, resolveConnection) {
224
+ function createOwnerHttpClient(owner, baseUrl, credential, resolveConnection, shutdownSignal) {
211
225
  const connection = async () => {
212
226
  const resolved = resolveConnection
213
227
  ? await resolveConnection(owner)
@@ -319,7 +333,9 @@ function createOwnerHttpClient(owner, baseUrl, credential, resolveConnection) {
319
333
  ...(method === "POST"
320
334
  ? { body: JSON.stringify(requestBody) }
321
335
  : {}),
322
- signal: AbortSignal.timeout(OWNER_INVOKE_WATCHDOG_MS),
336
+ signal: shutdownSignal
337
+ ? AbortSignal.any([shutdownSignal, AbortSignal.timeout(OWNER_INVOKE_WATCHDOG_MS)])
338
+ : AbortSignal.timeout(OWNER_INVOKE_WATCHDOG_MS),
323
339
  }));
324
340
  },
325
341
  });
@@ -382,7 +398,7 @@ async function readDeploymentOwnerSummary(stateRoot) {
382
398
  return undefined;
383
399
  }
384
400
  }
385
- async function constructGraph(config, executionCredential, modelCredential, resolveConnection, resolveLocalToolConnection) {
401
+ async function constructGraph(config, executionCredential, modelCredential, resolveConnection, resolveLocalToolConnection, resolveMonitorControlConnection) {
386
402
  const sink = createOperationSink(config.stateRoot);
387
403
  const observer = createHostOperationObserver(entry => sink.write("platform-host", entry));
388
404
  const databasePath = join(config.stateRoot, "state", "task.sqlite");
@@ -445,9 +461,37 @@ async function constructGraph(config, executionCredential, modelCredential, reso
445
461
  taskStore.close();
446
462
  throw error;
447
463
  }
448
- const execution = observer.port("execution", createOwnerHttpClient("execution", config.executionBaseUrl, executionCredential, resolveConnection));
449
- const model = observer.port("model", createOwnerHttpClient("model", config.modelBaseUrl, modelCredential, resolveConnection));
464
+ const ownerShutdown = new AbortController();
465
+ const execution = observer.port("execution", createOwnerHttpClient("execution", config.executionBaseUrl, executionCredential, resolveConnection, ownerShutdown.signal));
466
+ const model = observer.port("model", createOwnerHttpClient("model", config.modelBaseUrl, modelCredential, resolveConnection, ownerShutdown.signal));
467
+ const monitor = resolveMonitorControlConnection
468
+ ? Object.freeze({
469
+ async invoke(operation, input = {}) {
470
+ const connection = await resolveMonitorControlConnection();
471
+ if (!connection)
472
+ throw new Error("MONITOR_CONTROL_UNAVAILABLE");
473
+ return createMonitorControlClient({
474
+ endpoint: connection.endpoint,
475
+ token: connection.credential,
476
+ }).invoke(operation, input);
477
+ },
478
+ })
479
+ : undefined;
450
480
  let reconciliationCoordinator;
481
+ let productCampaignContinuationCoordinator;
482
+ const monitorRotationCoordinator = monitor
483
+ ? createMonitorRotationCoordinator({
484
+ monitor,
485
+ execution,
486
+ onError: (errorCode) => sink.write("platform-host", {
487
+ timestamp: new Date().toISOString(),
488
+ level: "WARN",
489
+ component: "platform-host-monitor-rotation",
490
+ event: "MONITOR_ROTATION_POLL_FAILED",
491
+ errorCode,
492
+ }),
493
+ })
494
+ : undefined;
451
495
  const boundedSystemView = async (view) => {
452
496
  if (view === "task") {
453
497
  const tasks = unwrap(task.queries.listTasks({})).tasks;
@@ -649,8 +693,59 @@ async function constructGraph(config, executionCredential, modelCredential, reso
649
693
  throw Object.assign(new Error(code), { httpStatus });
650
694
  }
651
695
  }
652
- const taskOperation = taskOperations.get(operationId);
696
+ const taskOperation = operationId === "getProductDiscussionContext"
697
+ ? () => {
698
+ const session = agent.getProductDiscussionSession(string(input.discussionRef, "discussionRef"));
699
+ return { ok: true, data: { ...session, discussionGeneration: session.generation } };
700
+ }
701
+ : taskOperations.get(operationId);
653
702
  if (taskOperation) {
703
+ const productOperation = role.agentPackageRef === "@tomflow/proflow-agent-product" &&
704
+ productCampaignOperationIds.has(operationId)
705
+ ? canonicalizeProductOperation({
706
+ operationId,
707
+ authenticatedRoleRef,
708
+ rawInput: input,
709
+ getDiscussionSession: (discussionRef) => agent.getProductDiscussionSession(discussionRef),
710
+ getRoleBindings: () => rolePackageRefs.map((agentPackageRef) => ({
711
+ agentPackageRef,
712
+ roleRef: roleForPackage(agentPackageRef).roleRef,
713
+ })),
714
+ verifyDiscussionAdmission: (admission) => {
715
+ if (!productDiscussionHost?.verifyAdmission(admission))
716
+ return false;
717
+ return !unwrap(task.queries.listTasks({})).tasks.some((summary) => unwrap(task.queries.getTask({ taskId: summary.taskId })).roleBindings.some((binding) => binding.conversationLocator === admission.conversationLocator ||
718
+ binding.workerRef === admission.conversationRef));
719
+ },
720
+ })
721
+ : undefined;
722
+ if (productOperation?.scope === "PRODUCT_QUERY") {
723
+ if (operationId === "getCampaignAuthorization") {
724
+ const authorization = unwrap(task.queries.getCampaignAuthorization(productOperation.input));
725
+ if (authorization.campaignRef !== input.campaignRef ||
726
+ authorization.discussionRef !== input.discussionRef)
727
+ throw Object.assign(new Error("PRODUCT_DISCUSSION_CAMPAIGN_MISMATCH"), { httpStatus: 403 });
728
+ }
729
+ return taskOperation(productOperation.input);
730
+ }
731
+ if (productOperation?.scope === "PRODUCT_MUTATION") {
732
+ const taskResult = await taskOperation({
733
+ ...productOperation.input,
734
+ actorRef: productOperation.actorRef,
735
+ });
736
+ if (productOperation.kickContinuation &&
737
+ typeof taskResult === "object" &&
738
+ taskResult !== null &&
739
+ Reflect.get(taskResult, "ok") === true) {
740
+ const data = Reflect.get(taskResult, "data");
741
+ if (typeof data === "object" && data !== null) {
742
+ const taskId = Reflect.get(data, "taskId");
743
+ if (typeof taskId === "string")
744
+ productCampaignContinuationCoordinator?.kick(taskId);
745
+ }
746
+ }
747
+ return taskResult;
748
+ }
654
749
  let actorRef = authenticatedRoleRef;
655
750
  let canonicalWorkerRef;
656
751
  if (typeof input.taskId === "string") {
@@ -691,8 +786,10 @@ async function constructGraph(config, executionCredential, modelCredential, reso
691
786
  const taskResult = await taskOperation(queryOperations.has(operationId)
692
787
  ? canonicalTaskInput
693
788
  : { ...canonicalTaskInput, actorRef });
694
- if (taskMutationOperations.has(operationId) && typeof input.taskId === "string")
789
+ if (taskMutationOperations.has(operationId) && typeof input.taskId === "string") {
695
790
  reconciliationCoordinator?.kick(input.taskId);
791
+ productCampaignContinuationCoordinator?.kick(input.taskId);
792
+ }
696
793
  if (operationId === "getTaskDocument")
697
794
  return fileBridgeOutputForTaskResult(taskResult);
698
795
  return taskResult;
@@ -713,8 +810,14 @@ async function constructGraph(config, executionCredential, modelCredential, reso
713
810
  const declared = current.roleBindings.find((item) => item.roleRef === binding.roleRef);
714
811
  if (!declared)
715
812
  throw new Error("AGENT_PACKAGE_NOT_ELIGIBLE");
716
- if (declared.workerRef === binding.workerRef)
813
+ if (declared.workerRef === binding.workerRef) {
814
+ if (declared.conversationLocator !== null &&
815
+ declared.conversationLocator !== binding.conversationLocator)
816
+ throw new Error("TASK_ROLE_BINDING_CONVERSATION_MISMATCH");
817
+ if (declared.conversationLocator === binding.conversationLocator)
818
+ productDiscussionHost?.releaseTaskWorker(binding.taskId, binding.roleRef);
717
819
  return;
820
+ }
718
821
  if (declared.workerRef)
719
822
  throw new Error("TASK_ROLE_BINDING_CONFLICT");
720
823
  unwrap(task.commands.bindTaskWorker({
@@ -727,7 +830,9 @@ async function constructGraph(config, executionCredential, modelCredential, reso
727
830
  actorRef: "platform-host:worker-provisioning",
728
831
  idempotencyKey: `browser-bind:${binding.taskId}:${binding.roleRef}:${binding.workerRef}`,
729
832
  }));
833
+ productDiscussionHost?.releaseTaskWorker(binding.taskId, binding.roleRef);
730
834
  reconciliationCoordinator?.kick(binding.taskId);
835
+ productCampaignContinuationCoordinator?.kick(binding.taskId);
731
836
  },
732
837
  }),
733
838
  agent: Object.freeze({
@@ -764,6 +869,62 @@ async function constructGraph(config, executionCredential, modelCredential, reso
764
869
  });
765
870
  const authorizeExecution = async (request) => {
766
871
  try {
872
+ if (request.capability === "product.discussion.bootstrap.deliver")
873
+ return productDiscussionHost
874
+ ? productDiscussionHost.authorizeBootstrapExecution(request)
875
+ : false;
876
+ if (request.capability === "monitor.chat.submit" ||
877
+ request.capability === "monitor.chat.create") {
878
+ if (!monitor)
879
+ return false;
880
+ return authorizeMonitorExecution({ monitor, request: request });
881
+ }
882
+ if (request.capability === "product.review.deliver") {
883
+ if (request.callerRef !== "platform-host:campaign-continuation")
884
+ return false;
885
+ if (request.taskId !== undefined ||
886
+ request.nodeId !== undefined ||
887
+ request.runNo !== undefined ||
888
+ request.workerRef !== undefined ||
889
+ request.projectRoot !== undefined)
890
+ return false;
891
+ const reviewInput = object(request.input, "product review execution input");
892
+ const reviewRef = string(reviewInput.reviewRef, "input.reviewRef");
893
+ const discussionRef = string(reviewInput.discussionRef, "input.discussionRef");
894
+ const discussionGeneration = positiveInteger(reviewInput.discussionGeneration, "input.discussionGeneration");
895
+ const roleRef = string(reviewInput.roleRef, "input.roleRef");
896
+ const conversationRef = string(reviewInput.conversationRef, "input.conversationRef");
897
+ const conversationLocator = string(reviewInput.conversationLocator, "input.conversationLocator");
898
+ const campaignRef = string(reviewInput.campaignRef, "input.campaignRef");
899
+ const intentRef = string(reviewInput.intentRef, "input.intentRef");
900
+ const terminalTaskId = string(reviewInput.terminalTaskId, "input.terminalTaskId");
901
+ const terminalTaskVersion = positiveInteger(reviewInput.terminalTaskVersion, "input.terminalTaskVersion");
902
+ if (request.roleRef !== roleRef)
903
+ return false;
904
+ const productRole = agent
905
+ .listRegisteredRoles()
906
+ .find((candidate) => candidate.agentPackageRef === "@tomflow/proflow-agent-product");
907
+ if (!productRole || productRole.roleRef !== roleRef)
908
+ return false;
909
+ const session = agent.getProductDiscussionSession(discussionRef);
910
+ if (session.status !== "ACTIVE" ||
911
+ session.roleRef !== roleRef ||
912
+ session.generation !== discussionGeneration ||
913
+ session.conversationRef !== conversationRef ||
914
+ session.conversationLocator !== conversationLocator ||
915
+ session.campaignRef !== campaignRef ||
916
+ session.currentIntentRef !== intentRef)
917
+ return false;
918
+ const terminalTask = unwrap(task.queries.getTask({ taskId: terminalTaskId }));
919
+ if (!taskIsTerminal(terminalTask.status) ||
920
+ terminalTask.version !== terminalTaskVersion)
921
+ return false;
922
+ const expectedReviewRef = `product-review:${campaignRef}:${terminalTaskId}:${terminalTaskVersion}:${discussionRef}:${discussionGeneration}`;
923
+ if (reviewRef !== expectedReviewRef ||
924
+ reviewInput.contentFingerprint !== expectedReviewRef)
925
+ return false;
926
+ return true;
927
+ }
767
928
  const browserCapability = request.capability === "worker.create" ||
768
929
  request.capability === "worker.restore" ||
769
930
  request.capability === "worker.wake" ||
@@ -908,6 +1069,29 @@ async function constructGraph(config, executionCredential, modelCredential, reso
908
1069
  throw new Error("ROLE_NOT_FOUND");
909
1070
  return role;
910
1071
  };
1072
+ const productRole = agent
1073
+ .listRegisteredRoles()
1074
+ .find((candidate) => candidate.agentPackageRef === "@tomflow/proflow-agent-product");
1075
+ const productDiscussionHost = productRole
1076
+ ? createProductDiscussionHost({
1077
+ productRoleRef: productRole.roleRef,
1078
+ listTaskIds: () => unwrap(task.queries.listTasks({})).tasks.map((summary) => summary.taskId),
1079
+ getTaskFact: (taskId) => {
1080
+ const current = unwrap(task.queries.getTask({ taskId }));
1081
+ return {
1082
+ taskId: current.taskId,
1083
+ status: current.status,
1084
+ roleBindings: current.roleBindings,
1085
+ };
1086
+ },
1087
+ registerDiscussionSession: (input) => agent.registerProductDiscussionSession(input),
1088
+ getDiscussionSession: (discussionRef) => agent.getProductDiscussionSession(discussionRef),
1089
+ async executeCapability(request) {
1090
+ return execution.invoke("executeCapability", request);
1091
+ },
1092
+ })
1093
+ : undefined;
1094
+ await productDiscussionHost?.recoverReservations();
911
1095
  const browserPermissionRole = (roleRef) => {
912
1096
  const role = agent.listRegisteredRoles().find((candidate) => candidate.roleRef === roleRef);
913
1097
  if (!role || !rolePackageRefs.includes(role.agentPackageRef))
@@ -953,6 +1137,49 @@ async function constructGraph(config, executionCredential, modelCredential, reso
953
1137
  }
954
1138
  if (operation === "role.list")
955
1139
  return agent.listRegisteredRoles();
1140
+ if (operation === "product.campaign.authorize") {
1141
+ const discussionRef = string(value.discussionRef, "discussionRef");
1142
+ const campaignRef = string(value.campaignRef, "campaignRef");
1143
+ const goalRevision = positiveInteger(value.goalRevision, "goalRevision");
1144
+ const productRole = roleForPackage("@tomflow/proflow-agent-product");
1145
+ const before = agent.getProductDiscussionSession(discussionRef);
1146
+ if (before.status !== "ACTIVE" || before.roleRef !== productRole.roleRef)
1147
+ throw new Error("PRODUCT_DISCUSSION_SESSION_NOT_READY");
1148
+ const granted = unwrap(task.commands.createCampaignAuthorization({
1149
+ ...(value.authorizationRef === undefined
1150
+ ? {}
1151
+ : { authorizationRef: string(value.authorizationRef, "authorizationRef") }),
1152
+ campaignRef,
1153
+ discussionRef,
1154
+ goalRevision,
1155
+ goalSummary: string(value.goalSummary, "goalSummary"),
1156
+ scope: value.scope,
1157
+ constraints: value.constraints,
1158
+ actorRef: "trusted:product-control",
1159
+ idempotencyKey: string(value.idempotencyKey, "idempotencyKey"),
1160
+ }));
1161
+ const current = agent.getProductDiscussionSession(discussionRef);
1162
+ if (current.status !== "ACTIVE" || current.roleRef !== productRole.roleRef)
1163
+ throw new Error("PRODUCT_DISCUSSION_SESSION_NOT_READY");
1164
+ const discussionSession = current.campaignRef === campaignRef &&
1165
+ current.goalRevision === goalRevision
1166
+ ? current
1167
+ : agent.updateProductDiscussionSessionContext({
1168
+ discussionRef,
1169
+ expectedVersion: current.version,
1170
+ campaignRef,
1171
+ goalRevision,
1172
+ });
1173
+ return { ...granted, discussionSession };
1174
+ }
1175
+ if (operation === "product.campaign.revoke")
1176
+ return unwrap(task.commands.revokeCampaignAuthorization({
1177
+ authorizationRef: string(value.authorizationRef, "authorizationRef"),
1178
+ expectedAuthorizationVersion: positiveInteger(value.expectedAuthorizationVersion, "expectedAuthorizationVersion"),
1179
+ reason: string(value.reason, "reason"),
1180
+ actorRef: "trusted:product-control",
1181
+ idempotencyKey: string(value.idempotencyKey, "idempotencyKey"),
1182
+ }));
956
1183
  const agentPackageRef = string(value.agentPackageRef, "agentPackageRef");
957
1184
  const role = roleForPackage(agentPackageRef);
958
1185
  if (operation === "role.show")
@@ -989,9 +1216,12 @@ async function constructGraph(config, executionCredential, modelCredential, reso
989
1216
  const binding = current.roleBindings.find((candidate) => candidate.agentPackageRef === agentPackageRef);
990
1217
  if (!binding)
991
1218
  throw new Error("TASK_ROLE_BINDING_REQUIRED");
992
- if (binding.workerRef && binding.conversationLocator)
1219
+ if (binding.workerRef && binding.conversationLocator) {
1220
+ productDiscussionHost?.releaseTaskWorker(taskId, binding.roleRef);
993
1221
  return;
1222
+ }
994
1223
  const role = roleForPackage(agentPackageRef);
1224
+ productDiscussionHost?.reserveTaskWorker(taskId, role.roleRef);
995
1225
  const executionRecord = object(await execution.invoke("executeCapability", {
996
1226
  contract: "execution",
997
1227
  contractVersion: "1.0.0",
@@ -1007,12 +1237,18 @@ async function constructGraph(config, executionCredential, modelCredential, reso
1007
1237
  bootstrapFingerprint: `new-task:${taskId}:${agentPackageRef}`,
1008
1238
  },
1009
1239
  }), "worker create execution");
1010
- if (executionRecord.status !== "SUCCEEDED")
1240
+ if (executionRecord.status !== "SUCCEEDED") {
1241
+ current = unwrap(task.queries.getTask({ taskId }));
1242
+ const observed = current.roleBindings.find((candidate) => candidate.agentPackageRef === agentPackageRef);
1243
+ if (observed?.workerRef && observed.conversationLocator)
1244
+ productDiscussionHost?.releaseTaskWorker(taskId, observed.roleRef);
1011
1245
  throw new Error(`WORKER_CREATE_NOT_CONFIRMED:${String(executionRecord.status)}`);
1246
+ }
1012
1247
  current = unwrap(task.queries.getTask({ taskId }));
1013
1248
  const persisted = current.roleBindings.find((candidate) => candidate.agentPackageRef === agentPackageRef);
1014
1249
  if (!persisted?.workerRef || !persisted.conversationLocator)
1015
1250
  throw new Error("WORKER_CREATE_BINDING_NOT_PERSISTED");
1251
+ productDiscussionHost?.releaseTaskWorker(taskId, persisted.roleRef);
1016
1252
  };
1017
1253
  const waitFor = new Set(options?.waitFor ?? []);
1018
1254
  const shouldWait = (agentPackageRef) => waitFor.size === 0 || waitFor.has(roleForPackage(agentPackageRef).roleRef);
@@ -1081,6 +1317,166 @@ async function constructGraph(config, executionCredential, modelCredential, reso
1081
1317
  getProjection: (taskId) => taskDriverPorts.getTaskDriveProjection(taskId),
1082
1318
  requestWake: requestTaskWake,
1083
1319
  });
1320
+ productCampaignContinuationCoordinator = createProductCampaignContinuationCoordinator({
1321
+ async getProjection(taskId) {
1322
+ const result = task.queries.getProductContinuationProjection({ taskId });
1323
+ if (!result.ok) {
1324
+ const code = typeof result.error === "object" && result.error !== null
1325
+ ? Reflect.get(result.error, "code")
1326
+ : undefined;
1327
+ if (code === "PRODUCT_CAMPAIGN_NOT_FOUND")
1328
+ return null;
1329
+ throw new Error(`PRODUCT_CONTINUATION_PROJECTION_FAILED:${String(code ?? "UNKNOWN")}`);
1330
+ }
1331
+ return result.data;
1332
+ },
1333
+ async listTaskIds() {
1334
+ return unwrap(task.queries.listTasks({})).tasks.map((summary) => summary.taskId);
1335
+ },
1336
+ async ensureWorkers(taskId) {
1337
+ await ensureTaskWorkers(taskId);
1338
+ },
1339
+ async transferRequirement(input) {
1340
+ unwrap(task.commands.transferProductIntentToTask({
1341
+ ...input,
1342
+ actorRef: "platform:campaign-continuation",
1343
+ }));
1344
+ },
1345
+ async startTask(input) {
1346
+ unwrap(task.commands.startTask({
1347
+ taskId: input.taskId,
1348
+ expectedTaskVersion: input.expectedTaskVersion,
1349
+ startAuthority: {
1350
+ type: "CAMPAIGN_AUTHORIZATION",
1351
+ authorizationRef: input.authorizationRef,
1352
+ expectedAuthorizationVersion: input.expectedAuthorizationVersion,
1353
+ expectedCampaignVersion: input.expectedCampaignVersion,
1354
+ },
1355
+ actorRef: "platform:campaign-continuation",
1356
+ idempotencyKey: input.idempotencyKey,
1357
+ }));
1358
+ },
1359
+ getDiscussionSessionForCampaign: (campaignRef) => agent.getProductDiscussionSessionForCampaign(campaignRef),
1360
+ updateDiscussionSessionContext: (input) => agent.updateProductDiscussionSessionContext(input),
1361
+ kickTaskReconciliation: (taskId) => reconciliationCoordinator?.kick(taskId),
1362
+ async onProductReviewRequired(signal) {
1363
+ const session = agent.getProductDiscussionSession(signal.discussionRef);
1364
+ if (session.status !== "ACTIVE" ||
1365
+ session.roleRef !== signal.roleRef ||
1366
+ session.generation !== signal.discussionGeneration ||
1367
+ session.conversationRef !== signal.conversationRef ||
1368
+ session.conversationLocator !== signal.conversationLocator ||
1369
+ session.campaignRef !== signal.campaignRef ||
1370
+ session.currentIntentRef !== signal.intentRef)
1371
+ throw new Error("PRODUCT_REVIEW_DISCUSSION_STALE");
1372
+ const reviewRequest = {
1373
+ contract: "execution",
1374
+ contractVersion: "1.0.0",
1375
+ idempotencyKey: signal.reviewRef,
1376
+ callerRef: "platform-host:campaign-continuation",
1377
+ correlationId: signal.reviewRef,
1378
+ roleRef: signal.roleRef,
1379
+ capability: "product.review.deliver",
1380
+ input: {
1381
+ reviewRef: signal.reviewRef,
1382
+ discussionRef: signal.discussionRef,
1383
+ discussionGeneration: signal.discussionGeneration,
1384
+ roleRef: signal.roleRef,
1385
+ conversationRef: signal.conversationRef,
1386
+ conversationLocator: signal.conversationLocator,
1387
+ campaignRef: signal.campaignRef,
1388
+ intentRef: signal.intentRef,
1389
+ terminalTaskId: signal.taskId,
1390
+ terminalTaskVersion: signal.terminalTaskVersion,
1391
+ contentFingerprint: signal.reviewRef,
1392
+ },
1393
+ };
1394
+ if (!(await authorizeExecution(reviewRequest)))
1395
+ throw new Error("PRODUCT_REVIEW_EXECUTION_IDENTITY_DENIED");
1396
+ let executionRecord;
1397
+ try {
1398
+ executionRecord = object(await execution.invoke("executeCapability", reviewRequest), "product review delivery execution");
1399
+ }
1400
+ catch (error) {
1401
+ await sink.write("platform-host", {
1402
+ timestamp: new Date().toISOString(),
1403
+ level: "WARN",
1404
+ component: "platform-host-product-campaign-continuation",
1405
+ event: "PRODUCT_REVIEW_REQUIRED",
1406
+ eventId: signal.reviewRef,
1407
+ correlationId: signal.reviewRef,
1408
+ status: "UNKNOWN",
1409
+ taskId: signal.taskId,
1410
+ roleRef: signal.roleRef,
1411
+ conversationLocator: signal.conversationLocator,
1412
+ campaignRef: signal.campaignRef,
1413
+ intentRef: signal.intentRef,
1414
+ discussionRef: signal.discussionRef,
1415
+ discussionGeneration: signal.discussionGeneration,
1416
+ terminalTaskVersion: signal.terminalTaskVersion,
1417
+ errorCode: "EXECUTION_OWNER_RESPONSE_UNKNOWN",
1418
+ });
1419
+ throw error;
1420
+ }
1421
+ const status = string(executionRecord.status, "execution.status");
1422
+ const sideEffectState = string(executionRecord.sideEffectState, "execution.sideEffectState");
1423
+ const executionRef = typeof executionRecord.executionRef === "string"
1424
+ ? executionRecord.executionRef
1425
+ : undefined;
1426
+ const executionResult = typeof executionRecord.result === "object" &&
1427
+ executionRecord.result !== null &&
1428
+ !Array.isArray(executionRecord.result)
1429
+ ? executionRecord.result
1430
+ : undefined;
1431
+ const resultData = executionResult &&
1432
+ typeof executionResult.data === "object" &&
1433
+ executionResult.data !== null &&
1434
+ !Array.isArray(executionResult.data)
1435
+ ? executionResult.data
1436
+ : undefined;
1437
+ const evidenceRef = executionResult?.capability === "product.review.deliver" &&
1438
+ resultData?.reviewRef === signal.reviewRef &&
1439
+ resultData.delivered === true &&
1440
+ typeof resultData.evidenceRef === "string"
1441
+ ? resultData.evidenceRef
1442
+ : undefined;
1443
+ const delivered = status === "SUCCEEDED" &&
1444
+ sideEffectState === "APPLIED" &&
1445
+ evidenceRef !== undefined;
1446
+ const error = typeof executionRecord.error === "object" &&
1447
+ executionRecord.error !== null &&
1448
+ !Array.isArray(executionRecord.error)
1449
+ ? executionRecord.error
1450
+ : undefined;
1451
+ const errorCode = typeof error?.code === "string" ? error.code : undefined;
1452
+ await sink.write("platform-host", {
1453
+ timestamp: new Date().toISOString(),
1454
+ level: delivered ? "INFO" : "WARN",
1455
+ component: "platform-host-product-campaign-continuation",
1456
+ event: "PRODUCT_REVIEW_REQUIRED",
1457
+ eventId: signal.reviewRef,
1458
+ correlationId: signal.reviewRef,
1459
+ status: delivered
1460
+ ? "DELIVERED"
1461
+ : status === "UNKNOWN"
1462
+ ? "UNKNOWN"
1463
+ : "FAILED",
1464
+ taskId: signal.taskId,
1465
+ roleRef: signal.roleRef,
1466
+ conversationLocator: signal.conversationLocator,
1467
+ campaignRef: signal.campaignRef,
1468
+ intentRef: signal.intentRef,
1469
+ discussionRef: signal.discussionRef,
1470
+ discussionGeneration: signal.discussionGeneration,
1471
+ terminalTaskVersion: signal.terminalTaskVersion,
1472
+ ...(executionRef ? { executionRef } : {}),
1473
+ ...(evidenceRef ? { evidenceRef } : {}),
1474
+ ...(errorCode ? { errorCode } : {}),
1475
+ });
1476
+ if (!delivered)
1477
+ throw new Error(`PRODUCT_REVIEW_DELIVERY_NOT_CONFIRMED:${status}:${sideEffectState}`);
1478
+ },
1479
+ });
1084
1480
  const taskApplication = observer.port("task", {
1085
1481
  async invoke(operation, rawInput) {
1086
1482
  const value = object(rawInput, "task application input");
@@ -1243,6 +1639,18 @@ async function constructGraph(config, executionCredential, modelCredential, reso
1243
1639
  void reconciliationCoordinator?.sweep();
1244
1640
  return { scheduled: true };
1245
1641
  }
1642
+ if (operation === "product.discussion.register")
1643
+ throw new Error("PRODUCT_DISCUSSION_REGISTER_DIRECT_FORBIDDEN");
1644
+ if (operation === "product.discussion.observe") {
1645
+ if (!productDiscussionHost)
1646
+ throw new Error("PRODUCT_DISCUSSION_ADMISSION_UNAVAILABLE");
1647
+ return productDiscussionHost.observe({
1648
+ conversationLocator: string(value.conversationLocator, "conversationLocator"),
1649
+ contentInstanceId: string(value.contentInstanceId, "contentInstanceId"),
1650
+ });
1651
+ }
1652
+ if (operation === "product.discussion.get")
1653
+ return agent.getProductDiscussionSession(string(value.discussionRef, "discussionRef"));
1246
1654
  if (operation === "browser.permission.classify") {
1247
1655
  const roleRef = string(value.roleRef, "roleRef");
1248
1656
  if (value.taskId !== undefined && typeof value.taskId !== "string")
@@ -1374,6 +1782,7 @@ async function constructGraph(config, executionCredential, modelCredential, reso
1374
1782
  },
1375
1783
  });
1376
1784
  reconciliationCoordinator.start();
1785
+ productCampaignContinuationCoordinator.start();
1377
1786
  return Object.freeze({
1378
1787
  route,
1379
1788
  operationSink: sink,
@@ -1385,6 +1794,9 @@ async function constructGraph(config, executionCredential, modelCredential, reso
1385
1794
  taskApplication,
1386
1795
  approvalApplication,
1387
1796
  observerApplication,
1797
+ startMonitorRotation() {
1798
+ monitorRotationCoordinator?.start();
1799
+ },
1388
1800
  async lookup(_operationId, _authenticatedRoleRef, _input) {
1389
1801
  throw Object.assign(new Error("ACTION_RESULT_LOOKUP_UNSUPPORTED"), { httpStatus: 409 });
1390
1802
  },
@@ -1414,8 +1826,17 @@ async function constructGraph(config, executionCredential, modelCredential, reso
1414
1826
  model: modelStatus,
1415
1827
  };
1416
1828
  },
1417
- close() {
1829
+ async close() {
1830
+ productCampaignContinuationCoordinator?.stop();
1418
1831
  reconciliationCoordinator?.stop();
1832
+ await monitorRotationCoordinator?.stop();
1833
+ ownerShutdown.abort();
1834
+ await Promise.all([
1835
+ productCampaignContinuationCoordinator?.drain(),
1836
+ reconciliationCoordinator?.drain(),
1837
+ ]);
1838
+ productDiscussionHost?.clear();
1839
+ await sink.drain();
1419
1840
  agent.close();
1420
1841
  taskStore.close();
1421
1842
  },
@@ -1659,9 +2080,9 @@ export function createPlatformHost(input) {
1659
2080
  ? await readExecutionTransportCredential(input.config.executionTransportCredentialFile)
1660
2081
  : input.executionCredential;
1661
2082
  log("DEPENDENCY_INITIALIZATION_STARTED", {
1662
- order: ["task", "agent", "execution-client", "model-client"],
2083
+ order: ["task", "agent", "execution-client", "model-client", "monitor-control-client"],
1663
2084
  });
1664
- graph = await constructGraph(input.config, executionTransportCredential, modelTransportCredential, input.resolveOwnerConnection, input.resolveLocalToolConnection);
2085
+ graph = await constructGraph(input.config, executionTransportCredential, modelTransportCredential, input.resolveOwnerConnection, input.resolveLocalToolConnection, input.resolveMonitorControlConnection);
1665
2086
  server = createServer((request, response) => {
1666
2087
  const work = operationContext.run({ operationRef: incomingOperationRef(request.headers["x-proflow-operation-ref"]) }, async () => {
1667
2088
  const url = new URL(request.url ?? "/", "http://platform-host.local");
@@ -1875,6 +2296,7 @@ export function createPlatformHost(input) {
1875
2296
  const address = server.address();
1876
2297
  if (!address || typeof address === "string")
1877
2298
  throw new Error("platform-host missing TCP address");
2299
+ graph.startMonitorRotation();
1878
2300
  log("SERVICE_STARTED", { host: input.config.host, port: address.port });
1879
2301
  return { host: input.config.host, port: address.port };
1880
2302
  }
@@ -1882,7 +2304,7 @@ export function createPlatformHost(input) {
1882
2304
  accepting = false;
1883
2305
  server?.close();
1884
2306
  server = undefined;
1885
- graph?.close();
2307
+ await graph?.close();
1886
2308
  graph = undefined;
1887
2309
  roleManagementCredential = undefined;
1888
2310
  taskApplicationCredential = undefined;
@@ -1902,7 +2324,7 @@ export function createPlatformHost(input) {
1902
2324
  if (running)
1903
2325
  await new Promise((resolveStop, reject) => running.close((error) => (error ? reject(error) : resolveStop())));
1904
2326
  await Promise.allSettled([...active]);
1905
- graph?.close();
2327
+ await graph?.close();
1906
2328
  graph = undefined;
1907
2329
  roleManagementCredential = undefined;
1908
2330
  taskApplicationCredential = undefined;