pi-harness-runtime 1.1.323 → 1.1.324

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.
Files changed (107) hide show
  1. package/package.json +1 -1
  2. package/packages/a2a-adapter/package.json +1 -1
  3. package/packages/agent-policy/package.json +1 -1
  4. package/packages/agent-policy/test/policy-retention.test.ts +2 -2
  5. package/packages/architecture-generator/package.json +1 -1
  6. package/packages/auth/package.json +1 -1
  7. package/packages/autonomous-refactor/package.json +1 -1
  8. package/packages/autonomous-runtime/package.json +1 -1
  9. package/packages/cache-strategy/package.json +1 -1
  10. package/packages/capability-registry/package.json +1 -1
  11. package/packages/checkpoint/package.json +1 -1
  12. package/packages/cli-plugin-sdk/package.json +1 -1
  13. package/packages/clipboard/package.json +1 -1
  14. package/packages/clipboard-plugin/package.json +1 -1
  15. package/packages/code-generation/package.json +1 -1
  16. package/packages/code-review/package.json +1 -1
  17. package/packages/codex-adapter/package.json +1 -1
  18. package/packages/codex-watcher/package.json +1 -1
  19. package/packages/config-capture/package.json +1 -1
  20. package/packages/context-compiler/package.json +1 -1
  21. package/packages/context-discovery/package.json +1 -1
  22. package/packages/context-manager/package.json +1 -1
  23. package/packages/cookie-sanitizer/package.json +1 -1
  24. package/packages/cost-optimizer/package.json +1 -1
  25. package/packages/dependency-analyzer/package.json +1 -1
  26. package/packages/django-plugin/package.json +1 -1
  27. package/packages/doc-generator/package.json +1 -1
  28. package/packages/durable-review/dist/worker.d.ts +19 -5
  29. package/packages/durable-review/dist/worker.d.ts.map +1 -1
  30. package/packages/durable-review/dist/worker.js +179 -35
  31. package/packages/durable-review/dist/worker.js.map +1 -1
  32. package/packages/durable-review/package.json +1 -1
  33. package/packages/durable-review/src/worker.ts +201 -48
  34. package/packages/e2e-tester/package.json +1 -1
  35. package/packages/evaluation-engine/package.json +1 -1
  36. package/packages/evaluation-runner/package.json +1 -1
  37. package/packages/event-bus/package.json +1 -1
  38. package/packages/event-store/package.json +1 -1
  39. package/packages/experience-replay/package.json +1 -1
  40. package/packages/feedback-collector/package.json +1 -1
  41. package/packages/file-copy-helper/package.json +1 -1
  42. package/packages/framework-detector/package.json +1 -1
  43. package/packages/framework-plugin-sdk/package.json +1 -1
  44. package/packages/frappe-plugin/package.json +1 -1
  45. package/packages/generic-web-plugin/package.json +1 -1
  46. package/packages/health-monitor/package.json +1 -1
  47. package/packages/intent-analyzer/package.json +1 -1
  48. package/packages/jev-judge/package.json +1 -1
  49. package/packages/knowledge-graph/package.json +1 -1
  50. package/packages/knowledge-retrieval/package.json +1 -1
  51. package/packages/laravel-plugin/package.json +1 -1
  52. package/packages/learning-engine/package.json +1 -1
  53. package/packages/mcp-adapter/package.json +1 -1
  54. package/packages/memory-engine/package.json +1 -1
  55. package/packages/milestone-manager/package.json +1 -1
  56. package/packages/model-registry/package.json +1 -1
  57. package/packages/nextjs-plugin/package.json +1 -1
  58. package/packages/notification/package.json +1 -1
  59. package/packages/observability/package.json +1 -1
  60. package/packages/okf-indexer/package.json +1 -1
  61. package/packages/paperclip-integration/package.json +1 -1
  62. package/packages/performance-optimizer/package.json +1 -1
  63. package/packages/privilege-broker/package.json +1 -1
  64. package/packages/project-analyzer/package.json +1 -1
  65. package/packages/project-bootstrap/package.json +1 -1
  66. package/packages/projection-engine/package.json +1 -1
  67. package/packages/prompt-compiler/package.json +1 -1
  68. package/packages/prompt-versioning/package.json +1 -1
  69. package/packages/provider-adapter-sdk/package.json +1 -1
  70. package/packages/provider-router/package.json +1 -1
  71. package/packages/provider-selector/package.json +1 -1
  72. package/packages/providers/package.json +1 -1
  73. package/packages/qdrant-skills/package.json +1 -1
  74. package/packages/quota-manager/package.json +1 -1
  75. package/packages/rate-limiter/package.json +1 -1
  76. package/packages/react-vite-plugin/package.json +1 -1
  77. package/packages/release-manager/package.json +1 -1
  78. package/packages/requirement-compiler/package.json +1 -1
  79. package/packages/runtime/package.json +1 -1
  80. package/packages/scheduler/package.json +1 -1
  81. package/packages/scheduler-adapter/package.json +1 -1
  82. package/packages/session/package.json +1 -1
  83. package/packages/session-api/package.json +1 -1
  84. package/packages/session-export/package.json +1 -1
  85. package/packages/shared-context/package.json +1 -1
  86. package/packages/skill-mcp-client/package.json +1 -1
  87. package/packages/skill-registry/package.json +1 -1
  88. package/packages/skills/package.json +1 -1
  89. package/packages/sprint-planner/package.json +1 -1
  90. package/packages/subscription-engine/package.json +1 -1
  91. package/packages/task-compiler/package.json +1 -1
  92. package/packages/telegram/package.json +1 -1
  93. package/packages/tencentdb-memory/package.json +1 -1
  94. package/packages/tencentdb-sync/package.json +1 -1
  95. package/packages/test-data-generator/package.json +1 -1
  96. package/packages/test-generator/package.json +1 -1
  97. package/packages/todo-bd-sync/package.json +1 -1
  98. package/packages/token-estimation/package.json +1 -1
  99. package/packages/token-optimizer/package.json +1 -1
  100. package/packages/trajectory/package.json +1 -1
  101. package/packages/tui/package.json +1 -1
  102. package/packages/types/package.json +1 -1
  103. package/packages/webmcp-tester/package.json +1 -1
  104. package/packages/workflow-events/package.json +1 -1
  105. package/packages/workspace-scanner/package.json +1 -1
  106. package/packages/worktree/package.json +1 -1
  107. package/packages/write-review/package.json +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-harness-runtime",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "[BETA] Codex-style /usage status + autonomous coding harness for pi. Not production ready — expect breaking changes.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/a2a-adapter",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "type": "module",
5
5
  "description": "Google Agent-to-Agent (A2A) protocol adapter for pi-harness",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/agent-policy",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "Agent policy runtime for pi-harness - enforces mandatory rule delivery and mutation gating",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -308,7 +308,7 @@ describe("Policy Retention Tests", () => {
308
308
  const manifest = await loader.load(TEST_DIR);
309
309
 
310
310
  // Full text should include actual content, not just contract pointers
311
- const { renderFullPolicyText } = await import("../dist/policy-renderer.js");
311
+ const { renderFullPolicyText } = await import("../dist/index.js");
312
312
  const fullText = renderFullPolicyText(manifest);
313
313
 
314
314
  assert.ok(fullText.includes("Full Agents"), "Should include source title");
@@ -323,7 +323,7 @@ describe("Policy Retention Tests", () => {
323
323
  });
324
324
 
325
325
  const manifest = await loader.load(TEST_DIR);
326
- const { checkPolicyTextSize } = await import("../dist/policy-renderer.js");
326
+ const { checkPolicyTextSize } = await import("../dist/index.js");
327
327
 
328
328
  // With normal-sized content, should fit in 100KB limit
329
329
  const result = checkPolicyTextSize(manifest, 100 * 1024);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/architecture-generator",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "Generate ADR and Mermaid diagrams from project analysis",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/auth",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "pi-harness-runtime — auth",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/autonomous-refactor",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "type": "module",
5
5
  "description": "Autonomous refactoring engine",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/autonomous-runtime",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "Autonomous Operations Runtime — task inbox, lease management, and worker lifecycle (RFC-0101)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/cache-strategy",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "type": "module",
5
5
  "description": "Smart caching with TTL and invalidation strategies (RFC-0022)",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/capability-registry",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "Capability Registry for pi-harness-runtime — tracks model capabilities and enables capability-based routing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/checkpoint",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "Enhanced checkpoint system with incremental snapshots, diff-based storage, and recovery strategies",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/cli-plugin-sdk",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "type": "module",
5
5
  "description": "CLI Plugin SDK for pi-harness — install, list, remove, update, invoke plugins",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/clipboard",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/clipboard-plugin",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/code-generation",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "Code generation pipeline for pi-harness-runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/code-review",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "Automated code review engine for pi-harness-runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/codex-adapter",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "type": "module",
5
5
  "description": "OpenAI Codex provider adapter for pi-harness",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/codex-watcher",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "Watches Codex CLI sessions and emits visible messages for PING-PONG monitoring",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moocoding/config-capture",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "Auto-capture API-fetched configuration values to AGENTS.md",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/context-compiler",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "Bounded context selection engine for pi-harness-runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/context-discovery",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/context-manager",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "Context Window Manager — prevents model context overflow via compaction and resume prompts (RFC-0010)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/cookie-sanitizer",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "pi-harness-runtime — cookie sanitizer (provider-quota cookie file normaliser)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/cost-optimizer",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "Cost Optimizer for pi-harness-runtime — budget tracking, forecasting, and model selection optimization",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/dependency-analyzer",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/django-plugin",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "Django framework analysis — models, management commands, settings",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/doc-generator",
3
- "version": "1.1.323",
3
+ "version": "1.1.324",
4
4
  "description": "Auto-Doc Generator — generate documentation from source code (RFC-0014)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,21 +1,33 @@
1
1
  /**
2
2
  * Durable Review Worker
3
3
  *
4
- * Node worker entry point with exclusive ownership and Harness integration.
4
+ * Node worker entry point with proper exclusive ownership and Harness integration.
5
+ * Uses atomic lock acquisition (mkdir) and process liveness verification.
5
6
  */
6
7
  import type { ReviewRequest, ReviewStatusResponse } from "./types.js";
7
8
  /**
8
- * Try to acquire exclusive ownership of the workspace database
9
- * Uses a simple file-based lock with stale detection
9
+ * Lock acquisition result
10
10
  */
11
- export declare function tryAcquireOwnership(workspaceKey: string, workerId: string): Promise<{
11
+ interface LockResult {
12
12
  acquired: boolean;
13
13
  existingOwner?: string;
14
- }>;
14
+ staleLock?: boolean;
15
+ }
16
+ /**
17
+ * Try to acquire exclusive ownership using file creation
18
+ *
19
+ * Uses exclusive file creation for atomic lock acquisition.
20
+ * The process must be alive (verified by PID check) before taking over a stale lock.
21
+ */
22
+ export declare function tryAcquireOwnership(workspaceKey: string, workerId: string): Promise<LockResult>;
15
23
  /**
16
24
  * Release ownership of the workspace database
17
25
  */
18
26
  export declare function releaseOwnership(workspaceKey: string, workerId: string): Promise<void>;
27
+ /**
28
+ * Verify we still hold the lock (for worker health checks)
29
+ */
30
+ export declare function verifyLockOwnership(workspaceKey: string, workerId: string): boolean;
19
31
  /**
20
32
  * Start the worker for a workspace
21
33
  */
@@ -23,6 +35,7 @@ export declare function startWorker(workspacePath: string): Promise<{
23
35
  success: boolean;
24
36
  workerId?: string;
25
37
  workspaceKey?: string;
38
+ harness?: unknown;
26
39
  error?: string;
27
40
  }>;
28
41
  /**
@@ -53,4 +66,5 @@ export declare function getJobStatus(_jobId: string): Promise<ReviewStatusRespon
53
66
  * Cancel a review job
54
67
  */
55
68
  export declare function cancelJob(_jobId: string): Promise<boolean>;
69
+ export {};
56
70
  //# sourceMappingURL=worker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAuCtE;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA0CxD;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAa5F;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC;IAChE,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CAsBD;AAED;;GAEG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAKhD;AAED;;GAEG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC;IAChD,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CAcD;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,CAAC,CA2B1D;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAGvF;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAGhE"}
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAiCtE;;GAEG;AACH,UAAU,UAAU;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAqCD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CAuFrB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAa5F;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CASnF;AAMD;;GAEG;AACH,wBAAsB,WAAW,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC;IAChE,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CAmDD;AAED;;GAEG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAchD;AAED;;GAEG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC;IAChD,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CAoBD;AAMD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,CAAC,CA2B1D;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAGvF;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAGhE"}
@@ -1,60 +1,143 @@
1
1
  /**
2
2
  * Durable Review Worker
3
3
  *
4
- * Node worker entry point with exclusive ownership and Harness integration.
4
+ * Node worker entry point with proper exclusive ownership and Harness integration.
5
+ * Uses atomic lock acquisition (mkdir) and process liveness verification.
5
6
  */
6
7
  import { randomUUID } from "node:crypto";
7
8
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
8
9
  import { join } from "node:path";
9
- import { getWorkspaceDir, hashWorkspaceKey, validateAndHashRequest, toStatusResponse, createJobRecord, } from "./service.js";
10
+ import { getDatabasePath, getWorkspaceDir, hashWorkspaceKey, validateAndHashRequest, toStatusResponse, createJobRecord } from "./service.js";
10
11
  /**
11
12
  * Current worker state
12
13
  */
13
14
  let currentWorker = null;
15
+ // ---------------------------------------------------------------------------
16
+ // Atomic Lock Operations
17
+ // ---------------------------------------------------------------------------
14
18
  /**
15
- * Get lock file path for ownership
19
+ * Get lock file path for workspace
20
+ * Single lock file per workspace - only one worker can hold it at a time
16
21
  */
17
22
  function getLockPath(workspaceKey) {
18
23
  return join(getWorkspaceDir(workspaceKey), ".worker.lock");
19
24
  }
20
25
  /**
21
- * Try to acquire exclusive ownership of the workspace database
22
- * Uses a simple file-based lock with stale detection
26
+ * Check if a process is alive (Unix)
27
+ */
28
+ function isProcessAlive(pid) {
29
+ try {
30
+ // Unix: sending signal 0 checks if process exists
31
+ process.kill(pid, 0);
32
+ return true;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ /**
39
+ * Verify lock owner is still alive
40
+ */
41
+ function verifyOwnerAlive(lock) {
42
+ if (lock.pid === process.pid) {
43
+ return true; // Same process
44
+ }
45
+ return isProcessAlive(lock.pid);
46
+ }
47
+ /**
48
+ * Try to acquire exclusive ownership using file creation
49
+ *
50
+ * Uses exclusive file creation for atomic lock acquisition.
51
+ * The process must be alive (verified by PID check) before taking over a stale lock.
23
52
  */
24
53
  export async function tryAcquireOwnership(workspaceKey, workerId) {
54
+ const lockDir = join(getWorkspaceDir(workspaceKey), "locks");
25
55
  const lockPath = getLockPath(workspaceKey);
26
- const lockDir = getWorkspaceDir(workspaceKey);
27
- // Ensure directory exists
56
+ const lockContent = {
57
+ workerId,
58
+ pid: process.pid,
59
+ startedAt: Date.now(),
60
+ lockId: randomUUID(), // Unique ID for this lock instance
61
+ };
62
+ // Ensure lock directory exists
28
63
  if (!existsSync(lockDir)) {
29
64
  mkdirSync(lockDir, { recursive: true });
30
65
  }
31
66
  try {
32
- // Check if lock exists
67
+ // Check if our lock file already exists
33
68
  if (existsSync(lockPath)) {
34
- const existing = JSON.parse(readFileSync(lockPath, "utf-8"));
35
- // If same worker, we already own it
36
- if (existing.workerId === workerId) {
69
+ const existing = JSON.parse(readFileSync(lockPath, "utf8"));
70
+ // Same worker - we already have the lock
71
+ if (existing.workerId === workerId && existing.pid === process.pid) {
72
+ console.log("[lock] Same worker, returning acquired");
37
73
  return { acquired: true };
38
74
  }
39
- // Check if lock is stale (older than 60 seconds)
40
- const lockAge = Date.now() - existing.startedAt;
41
- if (lockAge < 60_000) {
42
- // Lock is recent, someone else owns it
75
+ // Different worker - check if owner is still alive
76
+ if (verifyOwnerAlive(existing)) {
77
+ // Owner is alive, we can't take the lock
78
+ return { acquired: false, existingOwner: existing.workerId };
79
+ }
80
+ // Owner is dead - we can take over
81
+ // Write new lock content atomically via rename
82
+ const tmpPath = lockPath + ".tmp." + process.pid;
83
+ writeFileSync(tmpPath, JSON.stringify(lockContent), "utf8");
84
+ try {
85
+ // Rename is atomic on POSIX when destination is on same filesystem
86
+ // This takes over the lock
87
+ const { renameSync } = await import("node:fs");
88
+ renameSync(tmpPath, lockPath);
89
+ return { acquired: true, staleLock: true };
90
+ }
91
+ catch {
92
+ // Takeover failed (another process beat us)
93
+ try {
94
+ unlinkSync(tmpPath);
95
+ }
96
+ catch { /* ignore */ }
43
97
  return { acquired: false, existingOwner: existing.workerId };
44
98
  }
45
- // Lock is stale, we can take over
46
99
  }
47
- // Write new lock file
48
- const lockContent = {
49
- workerId,
50
- pid: process.pid,
51
- startedAt: Date.now(),
52
- };
53
- writeFileSync(lockPath, JSON.stringify(lockContent), "utf-8");
100
+ // Lock doesn't exist - create it atomically
101
+ // Use writeFileSync with flag 'wx' for exclusive create
102
+ const { writeFileSync: wfSync } = await import("node:fs");
103
+ wfSync(lockPath, JSON.stringify(lockContent), { flag: "wx" });
54
104
  return { acquired: true };
55
105
  }
56
- catch (error) {
57
- console.error("Error acquiring lock:", error);
106
+ catch (err) {
107
+ const error = err;
108
+ if (error.code === "EEXIST") {
109
+ // Race: another process created it first
110
+ try {
111
+ const existing = JSON.parse(readFileSync(lockPath, "utf8"));
112
+ if (verifyOwnerAlive(existing)) {
113
+ return { acquired: false, existingOwner: existing.workerId };
114
+ }
115
+ // Owner is dead, try takeover
116
+ const takeoverContent = {
117
+ ...lockContent,
118
+ lockId: randomUUID(),
119
+ };
120
+ const tmpPath = lockPath + ".tmp." + process.pid;
121
+ writeFileSync(tmpPath, JSON.stringify(takeoverContent), "utf8");
122
+ try {
123
+ const { renameSync } = await import("node:fs");
124
+ renameSync(tmpPath, lockPath);
125
+ return { acquired: true, staleLock: true };
126
+ }
127
+ catch {
128
+ try {
129
+ unlinkSync(tmpPath);
130
+ }
131
+ catch { /* ignore */ }
132
+ return { acquired: false, existingOwner: existing.workerId };
133
+ }
134
+ }
135
+ catch {
136
+ return { acquired: false };
137
+ }
138
+ }
139
+ // Other error
140
+ console.error("[durable-review] Lock acquisition error:", error);
58
141
  return { acquired: false };
59
142
  }
60
143
  }
@@ -65,7 +148,7 @@ export async function releaseOwnership(workspaceKey, workerId) {
65
148
  const lockPath = getLockPath(workspaceKey);
66
149
  try {
67
150
  if (existsSync(lockPath)) {
68
- const existing = JSON.parse(readFileSync(lockPath, "utf-8"));
151
+ const existing = JSON.parse(readFileSync(lockPath, "utf8"));
69
152
  if (existing.workerId === workerId) {
70
153
  unlinkSync(lockPath);
71
154
  }
@@ -75,14 +158,32 @@ export async function releaseOwnership(workspaceKey, workerId) {
75
158
  // Ignore errors on release
76
159
  }
77
160
  }
161
+ /**
162
+ * Verify we still hold the lock (for worker health checks)
163
+ */
164
+ export function verifyLockOwnership(workspaceKey, workerId) {
165
+ const lockPath = getLockPath(workspaceKey);
166
+ try {
167
+ if (!existsSync(lockPath))
168
+ return false;
169
+ const existing = JSON.parse(readFileSync(lockPath, "utf8"));
170
+ return existing.workerId === workerId && existing.pid === process.pid;
171
+ }
172
+ catch {
173
+ return false;
174
+ }
175
+ }
176
+ // ---------------------------------------------------------------------------
177
+ // Worker Lifecycle
178
+ // ---------------------------------------------------------------------------
78
179
  /**
79
180
  * Start the worker for a workspace
80
181
  */
81
182
  export async function startWorker(workspacePath) {
82
- const canonicalPath = workspacePath; // TODO: resolve realpath
183
+ const canonicalPath = workspacePath;
83
184
  const workspaceKey = hashWorkspaceKey(canonicalPath);
84
185
  const workerId = `worker-${randomUUID().slice(0, 8)}`;
85
- // Try to acquire ownership
186
+ // Try to acquire exclusive ownership
86
187
  const lockResult = await tryAcquireOwnership(workspaceKey, workerId);
87
188
  if (!lockResult.acquired) {
88
189
  return {
@@ -90,19 +191,54 @@ export async function startWorker(workspacePath) {
90
191
  error: `Workspace is owned by ${lockResult.existingOwner || "another worker"}. Stop it first or try again later.`,
91
192
  };
92
193
  }
93
- // Set up worker state
94
- currentWorker = {
95
- workerId,
96
- workspaceKey,
97
- ready: true,
98
- };
99
- return { success: true, workerId, workspaceKey };
194
+ if (lockResult.staleLock) {
195
+ console.log(`[durable-review] Took over stale lock from ${lockResult.existingOwner}`);
196
+ }
197
+ try {
198
+ // Open SQLite storage using pi-durable's Node adapter
199
+ const { openNodeSqliteStorage } = await import("@earendil-works/pi-durable/storage/sqlite/node");
200
+ const dbPath = getDatabasePath(workspaceKey);
201
+ const storage = await openNodeSqliteStorage(dbPath);
202
+ // Create registry with reviewer extension
203
+ const { createRegistry } = await import("@earendil-works/pi-durable");
204
+ const registry = createRegistry();
205
+ // Open Harness with storage
206
+ const { Harness: HarnessClass } = await import("@earendil-works/pi-durable");
207
+ const { BACKGROUND_CONTEXT } = await import("@earendil-works/chord/context");
208
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
209
+ const harness = await HarnessClass.open(storage, { registry }, BACKGROUND_CONTEXT);
210
+ currentWorker = {
211
+ workerId,
212
+ workspaceKey,
213
+ harness,
214
+ storage,
215
+ ready: true,
216
+ };
217
+ return { success: true, workerId, workspaceKey, harness };
218
+ }
219
+ catch (err) {
220
+ // Release lock on failure
221
+ await releaseOwnership(workspaceKey, workerId);
222
+ return {
223
+ success: false,
224
+ error: err instanceof Error ? err.message : String(err),
225
+ };
226
+ }
100
227
  }
101
228
  /**
102
229
  * Stop the worker
103
230
  */
104
231
  export async function stopWorker() {
105
232
  if (currentWorker) {
233
+ try {
234
+ const { BACKGROUND_CONTEXT } = await import("@earendil-works/chord/context");
235
+ if (currentWorker.harness) {
236
+ await currentWorker.harness.close(BACKGROUND_CONTEXT);
237
+ }
238
+ }
239
+ catch {
240
+ // Ignore close errors
241
+ }
106
242
  await releaseOwnership(currentWorker.workspaceKey, currentWorker.workerId);
107
243
  currentWorker = null;
108
244
  }
@@ -117,12 +253,20 @@ export async function checkWorkerReady() {
117
253
  if (!currentWorker.ready) {
118
254
  return { ready: false, workerId: currentWorker.workerId, error: "Worker not ready" };
119
255
  }
256
+ // Verify we still hold the lock
257
+ if (!verifyLockOwnership(currentWorker.workspaceKey, currentWorker.workerId)) {
258
+ currentWorker = null;
259
+ return { ready: false, error: "Lock lost" };
260
+ }
120
261
  return {
121
262
  ready: true,
122
263
  workerId: currentWorker.workerId,
123
264
  workspaceKey: currentWorker.workspaceKey,
124
265
  };
125
266
  }
267
+ // ---------------------------------------------------------------------------
268
+ // Review Operations
269
+ // ---------------------------------------------------------------------------
126
270
  /**
127
271
  * Handle incoming review request
128
272
  */