@tomflow/proflow-task-orchestration 0.1.11 → 0.1.13

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.
@@ -0,0 +1,2 @@
1
+ /** Only owner-generated, flat Markdown files under the workspace are accepted. */
2
+ export declare function productDocumentAbsolute(workspaceRoot: string, filePath: string): string;
@@ -0,0 +1,24 @@
1
+ import { lstatSync, realpathSync } from "node:fs";
2
+ import { resolve, sep } from "node:path";
3
+ /** Only owner-generated, flat Markdown files under the workspace are accepted. */
4
+ export function productDocumentAbsolute(workspaceRoot, filePath) {
5
+ if (!/^product-requirements\/[a-f0-9]{24}\.md$/.test(filePath))
6
+ throw new Error("PRODUCT_DOCUMENT_PATH_INVALID");
7
+ const workspace = realpathSync(workspaceRoot);
8
+ const directory = resolve(workspace, "product-requirements");
9
+ const absolute = resolve(workspace, filePath);
10
+ for (const path of [directory, absolute]) {
11
+ try {
12
+ const stat = lstatSync(path);
13
+ if (stat.isSymbolicLink() || (path === directory ? !stat.isDirectory() : !stat.isFile()))
14
+ throw new Error("PRODUCT_DOCUMENT_PATH_INVALID");
15
+ if (!realpathSync(path).startsWith(`${workspace}${sep}`))
16
+ throw new Error("PRODUCT_DOCUMENT_PATH_INVALID");
17
+ }
18
+ catch (error) {
19
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
20
+ throw error;
21
+ }
22
+ }
23
+ return absolute;
24
+ }
@@ -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 {
@@ -1016,11 +1039,13 @@ export function createTaskServices(options) {
1016
1039
  tx.nodes.update(reopened);
1017
1040
  for (const later of tx.nodes.listByTask(task.taskId))
1018
1041
  if (later.sequenceNo > target.sequenceNo) {
1042
+ const invalidatedStartedRun = later.startedAt !== null || later.workerRef !== null;
1019
1043
  preserveRunHistory(tx, later, later.status, timestamp);
1020
1044
  tx.nodes.update({
1021
1045
  ...later,
1022
1046
  status: "PENDING",
1023
1047
  version: later.version + 1,
1048
+ runNo: later.runNo + (invalidatedStartedRun ? 1 : 0),
1024
1049
  workerRef: null,
1025
1050
  resultSummary: null,
1026
1051
  errorCode: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomflow/proflow-task-orchestration",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -23,9 +23,9 @@
23
23
  "@tomflow/proflow-module-contract": "^0.1.13"
24
24
  },
25
25
  "devDependencies": {
26
- "@tomflow/proflow-task-migration-runner": "^0.1.11",
27
26
  "@tomflow/proflow-deployment-conformance": "^0.1.13",
28
- "@tomflow/proflow-task-store-sqlite": "^0.1.11"
27
+ "@tomflow/proflow-task-store-sqlite": "^0.1.12",
28
+ "@tomflow/proflow-task-migration-runner": "^0.1.11"
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.11",
6
+ "moduleVersion": "0.1.13",
7
7
  "kind": "library",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",