@tomflow/proflow-task-orchestration 0.1.10 → 0.1.12

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @tomflow/proflow-task-orchestration
2
2
 
3
+ ## 0.1.12
4
+
5
+ ### Patch Changes
6
+
7
+ - Prune empty Task-level document recovery directories after recovery journals converge so durable recovery leaves no orphan filesystem residue.
8
+
9
+ - Harden Task-scoped Worker identity and generation semantics so reopened or invalidated executions cannot reuse stale Worker/conversation ownership or a previously consumed run generation.
10
+
11
+ ## 0.1.11
12
+
13
+ ### Patch Changes
14
+
15
+ - Release the verified Real-3 B1 task recovery, exact execution identity, durable observer resume, bounded gateway error, and matching Agent Action contract fixes.
16
+
3
17
  ## 0.1.10
4
18
 
5
19
  ### Patch Changes
@@ -4,7 +4,7 @@ declare const success: () => {
4
4
  readonly ok: true;
5
5
  readonly status: "SUCCEEDED";
6
6
  readonly moduleRef: "task-orchestration";
7
- readonly moduleVersion: "0.1.10";
7
+ readonly moduleVersion: "0.1.12";
8
8
  };
9
9
  observedEffects: string[];
10
10
  };
@@ -17,7 +17,7 @@ export declare const behaviorAdapter: {
17
17
  ok: true;
18
18
  status: "SUCCEEDED";
19
19
  moduleRef: "task-orchestration";
20
- moduleVersion: "0.1.10";
20
+ moduleVersion: "0.1.12";
21
21
  data: {
22
22
  setupStatus: "READY";
23
23
  runtimeStatus: "NOT_APPLICABLE";
@@ -32,7 +32,7 @@ export declare const behaviorAdapter: {
32
32
  ok: true;
33
33
  status: "SUCCEEDED";
34
34
  moduleRef: "task-orchestration";
35
- moduleVersion: "0.1.10";
35
+ moduleVersion: "0.1.12";
36
36
  data: {
37
37
  docs: string;
38
38
  };
@@ -3,7 +3,7 @@ export declare const descriptor: {
3
3
  readonly contractVersion: "1.0.0";
4
4
  readonly moduleRef: "task-orchestration";
5
5
  readonly packageName: "@tomflow/proflow-task-orchestration";
6
- readonly moduleVersion: "0.1.10";
6
+ readonly moduleVersion: "0.1.12";
7
7
  readonly kind: "library";
8
8
  readonly templateVersion: "1.0.0";
9
9
  readonly platformCompatibility: ">=1.0.0 <2.0.0";
@@ -3,7 +3,7 @@ export const descriptor = {
3
3
  contractVersion: "1.0.0",
4
4
  moduleRef: "task-orchestration",
5
5
  packageName: "@tomflow/proflow-task-orchestration",
6
- moduleVersion: "0.1.10",
6
+ moduleVersion: "0.1.12",
7
7
  kind: "library",
8
8
  templateVersion: "1.0.0",
9
9
  platformCompatibility: ">=1.0.0 <2.0.0",
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { requiredTaskAgentPackageRefs } from "./model.js";
2
+ import { requiredTaskAgentPackageRefs, taskWaitTypes } from "./model.js";
3
3
  export const publicOperationNames = [
4
4
  "createTaskGroup",
5
5
  "getTaskGroup",
@@ -98,6 +98,14 @@ const schemas = {
98
98
  path: ["sequenceNo"],
99
99
  });
100
100
  }
101
+ input.roleBindings.forEach((binding, index) => {
102
+ if (binding.workerRef !== null || binding.conversationLocator !== null)
103
+ context.addIssue({
104
+ code: "custom",
105
+ message: "createTask roleBindings must start with workerRef and conversationLocator unset",
106
+ path: ["roleBindings", index],
107
+ });
108
+ });
101
109
  const declaredAgentPackageRefs = new Set(input.roleBindings.map((binding) => binding.agentPackageRef));
102
110
  if (declaredAgentPackageRefs.size !== requiredTaskAgentPackageRefs.length ||
103
111
  requiredTaskAgentPackageRefs.some((agentPackageRef) => !declaredAgentPackageRefs.has(agentPackageRef))) {
@@ -136,7 +144,7 @@ const schemas = {
136
144
  waitNode: z
137
145
  .object({
138
146
  ...nodeVersion,
139
- waitType: id,
147
+ waitType: z.enum(taskWaitTypes),
140
148
  reasonCode: id,
141
149
  message: id,
142
150
  relatedRef: id.optional(),
@@ -1,4 +1,6 @@
1
1
  export declare const requiredTaskAgentPackageRefs: readonly ["@tomflow/proflow-agent-product", "@tomflow/proflow-agent-controller-dev", "@tomflow/proflow-agent-test-ops"];
2
+ export declare const taskWaitTypes: readonly ["BUSINESS_CONFIRMATION", "REQUIREMENT_BLOCKED", "EXTERNAL_BUSINESS_DEPENDENCY"];
3
+ export type TaskWaitType = (typeof taskWaitTypes)[number];
2
4
  export type TaskGroupStatus = "READY" | "ACTIVE" | "SUCCEEDED";
3
5
  export type TaskStatus = "PENDING" | "READY" | "ACTIVE" | "WAITING" | "FAILED" | "PAUSED" | "SUCCEEDED" | "TERMINATED";
4
6
  export type NodeStatus = "PENDING" | "READY" | "IN_PROGRESS" | "WAITING" | "FAILED" | "SUCCEEDED" | "TERMINATED";
package/dist/src/model.js CHANGED
@@ -3,3 +3,8 @@ export const requiredTaskAgentPackageRefs = [
3
3
  "@tomflow/proflow-agent-controller-dev",
4
4
  "@tomflow/proflow-agent-test-ops",
5
5
  ];
6
+ export const taskWaitTypes = [
7
+ "BUSINESS_CONFIRMATION",
8
+ "REQUIREMENT_BLOCKED",
9
+ "EXTERNAL_BUSINESS_DEPENDENCY",
10
+ ];
@@ -144,6 +144,7 @@ export declare function createTaskServices(options: {
144
144
  } | null;
145
145
  canDrive: boolean;
146
146
  blockedReason: string | null;
147
+ resumeSignalRef: string | null;
147
148
  }>;
148
149
  getNodeContext: (raw: unknown) => TaskResult<{
149
150
  task: Pick<Task, "taskId" | "title" | "objective" | "status" | "version">;
@@ -180,6 +180,17 @@ function recomputeReadiness(tx, task, timestamp, workspaceRoot) {
180
180
  function matchesWorker(actorRef, workerRef) {
181
181
  return workerRef !== null && actorRef === `worker:${workerRef}`;
182
182
  }
183
+ function workerIdentityBoundToAnotherTask(tx, input) {
184
+ for (const task of tx.tasks.list()) {
185
+ if (task.taskId === input.taskId)
186
+ continue;
187
+ for (const binding of tx.roleBindings.listByTask(task.taskId))
188
+ if (binding.workerRef === input.workerRef ||
189
+ binding.conversationLocator === input.conversationLocator)
190
+ return true;
191
+ }
192
+ return false;
193
+ }
183
194
  export function createTaskServices(options) {
184
195
  const { store, workspaceRoot } = options;
185
196
  const now = options.now ?? (() => new Date().toISOString());
@@ -321,13 +332,22 @@ export function createTaskServices(options) {
321
332
  ? taskId
322
333
  : `id-${createHash("sha256").update(taskId).digest("hex")}`;
323
334
  const stageDocumentPath = (taskId, type) => `.proflow/recovery/task-create/${safeTaskId(taskId)}/${type.toLowerCase().replaceAll("_", "-")}.md`;
324
- const documentUpdateRecoveryRoot = (taskId, type) => `.proflow/recovery/task-document/${safeTaskId(taskId)}/${type.toLowerCase().replaceAll("_", "-")}`;
335
+ const documentUpdateRecoveryTaskRoot = (taskId) => `.proflow/recovery/task-document/${safeTaskId(taskId)}`;
336
+ const documentUpdateRecoveryRoot = (taskId, type) => `${documentUpdateRecoveryTaskRoot(taskId)}/${type.toLowerCase().replaceAll("_", "-")}`;
325
337
  const documentUpdateRecoveryPath = (taskId, type, idempotencyKey) => `${documentUpdateRecoveryRoot(taskId, type)}/${createHash("sha256").update(idempotencyKey).digest("hex")}`;
338
+ const cleanupDocumentRecoveryTaskRoot = (taskId) => {
339
+ const taskRootAbsolute = join(workspaceRoot, documentUpdateRecoveryTaskRoot(taskId));
340
+ if (existsSync(taskRootAbsolute) &&
341
+ readdirSync(taskRootAbsolute).length === 0)
342
+ rmSync(taskRootAbsolute, { recursive: true, force: true });
343
+ };
326
344
  const recoverPendingDocumentUpdates = (taskId, type) => {
327
345
  const recoveryRoot = documentUpdateRecoveryRoot(taskId, type);
328
346
  const recoveryRootAbsolute = join(workspaceRoot, recoveryRoot);
329
- if (!existsSync(recoveryRootAbsolute))
347
+ if (!existsSync(recoveryRootAbsolute)) {
348
+ cleanupDocumentRecoveryTaskRoot(taskId);
330
349
  return;
350
+ }
331
351
  const canonicalPath = relativeDocumentPath(taskId, type);
332
352
  const canonicalAbsolute = join(workspaceRoot, canonicalPath);
333
353
  const currentMetadata = store.read((tx) => tx.documents.get(taskId, type));
@@ -375,6 +395,7 @@ export function createTaskServices(options) {
375
395
  }
376
396
  if (readdirSync(recoveryRootAbsolute).length === 0)
377
397
  rmSync(recoveryRootAbsolute, { recursive: true, force: true });
398
+ cleanupDocumentRecoveryTaskRoot(taskId);
378
399
  };
379
400
  const cleanupCreateStage = (taskId) => {
380
401
  rmSync(join(workspaceRoot, `.proflow/recovery/task-create/${safeTaskId(taskId)}`), { recursive: true, force: true });
@@ -630,6 +651,8 @@ export function createTaskServices(options) {
630
651
  throw new DomainError("AGENT_PACKAGE_NOT_ELIGIBLE", "Agent package is not declared by the Task.");
631
652
  if (old.roleRef !== input.roleRef)
632
653
  throw new DomainError("ROLE_BINDING_MISMATCH", "RoleRef does not match the declared binding.");
654
+ if (workerIdentityBoundToAnotherTask(tx, input))
655
+ throw new DomainError("TASK_ROLE_BINDING_CONFLICT", "Worker or Conversation identity is already bound to another Task.");
633
656
  if (old.workerRef === input.workerRef &&
634
657
  old.conversationLocator === input.conversationLocator)
635
658
  return {
@@ -726,15 +749,24 @@ export function createTaskServices(options) {
726
749
  const current = task.currentNodeId
727
750
  ? tx.nodes.get(task.currentNodeId)
728
751
  : undefined;
752
+ if (task.status === "WAITING" && current?.status !== "WAITING")
753
+ throw new DomainError("TASK_INVALID_STATE", "A WAITING Task must have a current WAITING Node.");
729
754
  if (current?.status === "FAILED")
730
755
  throw new DomainError("TASK_INVALID_STATE", "A failed Node must be explicitly reopened before resume.");
731
- if (current?.status === "WAITING")
756
+ if (current?.status === "WAITING") {
757
+ const unresolved = tx.messages
758
+ .listPending()
759
+ .some((message) => message.taskId === task.taskId &&
760
+ message.nodeId === current.nodeId);
761
+ if (unresolved)
762
+ throw new DomainError("TASK_BLOCKER_UNRESOLVED", "The current Node still has an unresolved business blocker.");
732
763
  tx.nodes.update({
733
764
  ...current,
734
765
  status: "IN_PROGRESS",
735
766
  version: current.version + 1,
736
767
  updatedAt: timestamp,
737
768
  });
769
+ }
738
770
  const updated = {
739
771
  ...task,
740
772
  status: "ACTIVE",
@@ -910,6 +942,8 @@ export function createTaskServices(options) {
910
942
  checkVersion(node.version, input.expectedNodeVersion, "NODE_VERSION_CONFLICT");
911
943
  if (task.status !== "ACTIVE")
912
944
  throw new DomainError("TASK_INVALID_STATE", "Task must be ACTIVE before a Node can wait.");
945
+ if (task.currentNodeId !== node.nodeId)
946
+ throw new DomainError("NODE_NOT_CURRENT", "Only the current Node can enter WAITING.");
913
947
  if (node.status !== "IN_PROGRESS" ||
914
948
  !matchesWorker(input.actorRef, node.workerRef))
915
949
  throw new DomainError(!matchesWorker(input.actorRef, node.workerRef)
@@ -950,10 +984,14 @@ export function createTaskServices(options) {
950
984
  const node = requireNode(tx, input.taskId, input.nodeId);
951
985
  checkVersion(task.version, input.expectedTaskVersion, "TASK_VERSION_CONFLICT");
952
986
  checkVersion(node.version, input.expectedNodeVersion, "NODE_VERSION_CONFLICT");
953
- if (!["ACTIVE", "WAITING"].includes(task.status))
954
- throw new DomainError("TASK_INVALID_STATE", "Task cannot accept a Node failure in its current state.");
955
- if (!["IN_PROGRESS", "WAITING"].includes(node.status))
956
- throw new DomainError("NODE_INVALID_STATE", "Node cannot fail.");
987
+ if (task.status !== "ACTIVE")
988
+ throw new DomainError("TASK_INVALID_STATE", "Task must be ACTIVE before the current run can fail.");
989
+ if (task.currentNodeId !== node.nodeId)
990
+ throw new DomainError("NODE_NOT_CURRENT", "Only the current Node can record a run failure.");
991
+ if (!matchesWorker(input.actorRef, node.workerRef))
992
+ throw new DomainError("WORKER_MISMATCH", "Only the bound running Worker may fail the current Node.");
993
+ if (node.status !== "IN_PROGRESS")
994
+ throw new DomainError("NODE_INVALID_STATE", "Only the current IN_PROGRESS run can fail.");
957
995
  const failedNode = {
958
996
  ...node,
959
997
  status: "FAILED",
@@ -1001,11 +1039,13 @@ export function createTaskServices(options) {
1001
1039
  tx.nodes.update(reopened);
1002
1040
  for (const later of tx.nodes.listByTask(task.taskId))
1003
1041
  if (later.sequenceNo > target.sequenceNo) {
1042
+ const invalidatedStartedRun = later.startedAt !== null || later.workerRef !== null;
1004
1043
  preserveRunHistory(tx, later, later.status, timestamp);
1005
1044
  tx.nodes.update({
1006
1045
  ...later,
1007
1046
  status: "PENDING",
1008
1047
  version: later.version + 1,
1048
+ runNo: later.runNo + (invalidatedStartedRun ? 1 : 0),
1009
1049
  workerRef: null,
1010
1050
  resultSummary: null,
1011
1051
  errorCode: null,
@@ -1116,6 +1156,13 @@ export function createTaskServices(options) {
1116
1156
  else if (!terminal && !roleBinding?.workerRef)
1117
1157
  blockedReason = "TASK_ROLE_BINDING_REQUIRED";
1118
1158
  const canDrive = !terminal && blockedReason === null;
1159
+ let durableResumeEventId;
1160
+ if (task.status === "ACTIVE" && currentNode?.status === "IN_PROGRESS")
1161
+ for (const taskEvent of tx.events.listByTask(task.taskId))
1162
+ if (taskEvent.eventType === "TASK_RESUMED" &&
1163
+ taskEvent.taskVersion === task.version &&
1164
+ taskEvent.eventId !== undefined)
1165
+ durableResumeEventId = taskEvent.eventId;
1119
1166
  return {
1120
1167
  taskId: task.taskId,
1121
1168
  taskStatus: task.status,
@@ -1140,6 +1187,9 @@ export function createTaskServices(options) {
1140
1187
  : null,
1141
1188
  canDrive,
1142
1189
  blockedReason,
1190
+ resumeSignalRef: durableResumeEventId === undefined
1191
+ ? null
1192
+ : `task-event:${durableResumeEventId}`,
1143
1193
  };
1144
1194
  }),
1145
1195
  getNodeContext: (raw) => queryWithDocumentRecovery("getNodeContext", raw, (tx, input) => {
@@ -7,7 +7,7 @@ const taskTransitions = {
7
7
  READY: ["ACTIVE", "TERMINATED"],
8
8
  ACTIVE: ["WAITING", "PAUSED", "FAILED", "SUCCEEDED", "TERMINATED"],
9
9
  WAITING: ["ACTIVE", "PAUSED", "TERMINATED"],
10
- FAILED: ["PAUSED", "TERMINATED"],
10
+ FAILED: ["TERMINATED"],
11
11
  PAUSED: ["ACTIVE", "TERMINATED"],
12
12
  SUCCEEDED: [],
13
13
  TERMINATED: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomflow/proflow-task-orchestration",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -25,7 +25,7 @@
25
25
  "devDependencies": {
26
26
  "@tomflow/proflow-deployment-conformance": "^0.1.13",
27
27
  "@tomflow/proflow-task-migration-runner": "^0.1.11",
28
- "@tomflow/proflow-task-store-sqlite": "^0.1.11"
28
+ "@tomflow/proflow-task-store-sqlite": "^0.1.12"
29
29
  },
30
30
  "description": "Owns the Task & Orchestration public contract, domain rules and application services.",
31
31
  "keywords": [
@@ -3,7 +3,7 @@
3
3
  "contractVersion": "1.0.0",
4
4
  "moduleRef": "task-orchestration",
5
5
  "packageName": "@tomflow/proflow-task-orchestration",
6
- "moduleVersion": "0.1.10",
6
+ "moduleVersion": "0.1.12",
7
7
  "kind": "library",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",