pi-harness-runtime 0.9.15 → 0.9.16
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/package.json +1 -1
- package/packages/autonomous-runtime/README.md +48 -0
- package/packages/autonomous-runtime/dist/inbox.d.ts +75 -0
- package/packages/autonomous-runtime/dist/inbox.d.ts.map +1 -0
- package/packages/autonomous-runtime/dist/inbox.js +212 -0
- package/packages/autonomous-runtime/dist/inbox.js.map +1 -0
- package/packages/autonomous-runtime/dist/index.d.ts +17 -0
- package/packages/autonomous-runtime/dist/index.d.ts.map +1 -0
- package/packages/autonomous-runtime/dist/index.js +17 -0
- package/packages/autonomous-runtime/dist/index.js.map +1 -0
- package/packages/autonomous-runtime/dist/lease.d.ts +78 -0
- package/packages/autonomous-runtime/dist/lease.d.ts.map +1 -0
- package/packages/autonomous-runtime/dist/lease.js +273 -0
- package/packages/autonomous-runtime/dist/lease.js.map +1 -0
- package/packages/autonomous-runtime/dist/types.d.ts +160 -0
- package/packages/autonomous-runtime/dist/types.d.ts.map +1 -0
- package/packages/autonomous-runtime/dist/types.js +31 -0
- package/packages/autonomous-runtime/dist/types.js.map +1 -0
- package/packages/autonomous-runtime/dist/worker.d.ts +63 -0
- package/packages/autonomous-runtime/dist/worker.d.ts.map +1 -0
- package/packages/autonomous-runtime/dist/worker.js +133 -0
- package/packages/autonomous-runtime/dist/worker.js.map +1 -0
- package/packages/autonomous-runtime/package.json +34 -0
- package/packages/autonomous-runtime/src/inbox.ts +266 -0
- package/packages/autonomous-runtime/src/index.ts +22 -0
- package/packages/autonomous-runtime/src/lease.ts +318 -0
- package/packages/autonomous-runtime/src/types.ts +257 -0
- package/packages/autonomous-runtime/test/autonomous-runtime.test.ts +194 -0
- package/packages/autonomous-runtime/test/inbox.test.mjs +266 -0
- package/packages/autonomous-runtime/test/inbox.test.ts +80 -0
- package/packages/autonomous-runtime/test/types.test.ts +68 -0
- package/packages/autonomous-runtime/tsconfig.json +22 -0
- package/packages/autonomous-runtime/tsconfig.tsbuildinfo +1 -0
- package/packages/privilege-broker/README.md +45 -0
- package/packages/privilege-broker/config/privileges.yaml.example +172 -0
- package/packages/privilege-broker/dist/approval.d.ts +23 -0
- package/packages/privilege-broker/dist/approval.d.ts.map +1 -0
- package/packages/privilege-broker/dist/approval.js +45 -0
- package/packages/privilege-broker/dist/approval.js.map +1 -0
- package/packages/privilege-broker/dist/audit.d.ts +38 -0
- package/packages/privilege-broker/dist/audit.d.ts.map +1 -0
- package/packages/privilege-broker/dist/audit.js +125 -0
- package/packages/privilege-broker/dist/audit.js.map +1 -0
- package/packages/privilege-broker/dist/executor.d.ts +25 -0
- package/packages/privilege-broker/dist/executor.d.ts.map +1 -0
- package/packages/privilege-broker/dist/executor.js +83 -0
- package/packages/privilege-broker/dist/executor.js.map +1 -0
- package/packages/privilege-broker/dist/index.d.ts +30 -0
- package/packages/privilege-broker/dist/index.d.ts.map +1 -0
- package/packages/privilege-broker/dist/index.js +5 -0
- package/packages/privilege-broker/dist/index.js.map +1 -0
- package/packages/privilege-broker/dist/registry.d.ts +15 -0
- package/packages/privilege-broker/dist/registry.d.ts.map +1 -0
- package/packages/privilege-broker/dist/registry.js +76 -0
- package/packages/privilege-broker/dist/registry.js.map +1 -0
- package/packages/privilege-broker/dist/types.d.ts +77 -0
- package/packages/privilege-broker/dist/types.d.ts.map +1 -0
- package/packages/privilege-broker/dist/types.js +11 -0
- package/packages/privilege-broker/dist/types.js.map +1 -0
- package/packages/privilege-broker/package.json +35 -0
- package/packages/privilege-broker/src/approval.ts +70 -0
- package/packages/privilege-broker/src/audit.ts +142 -0
- package/packages/privilege-broker/src/executor.ts +110 -0
- package/packages/privilege-broker/src/index.ts +46 -0
- package/packages/privilege-broker/src/registry.ts +98 -0
- package/packages/privilege-broker/src/types.ts +105 -0
- package/packages/privilege-broker/tsconfig.json +22 -0
- package/packages/privilege-broker/tsconfig.tsbuildinfo +1 -0
- package/packages/scheduler-adapter/README.md +36 -0
- package/packages/scheduler-adapter/dist/cron.d.ts +21 -0
- package/packages/scheduler-adapter/dist/cron.d.ts.map +1 -0
- package/packages/scheduler-adapter/dist/cron.js +116 -0
- package/packages/scheduler-adapter/dist/cron.js.map +1 -0
- package/packages/scheduler-adapter/dist/index.d.ts +9 -0
- package/packages/scheduler-adapter/dist/index.d.ts.map +1 -0
- package/packages/scheduler-adapter/dist/index.js +8 -0
- package/packages/scheduler-adapter/dist/index.js.map +1 -0
- package/packages/scheduler-adapter/dist/interface.d.ts +61 -0
- package/packages/scheduler-adapter/dist/interface.d.ts.map +1 -0
- package/packages/scheduler-adapter/dist/interface.js +2 -0
- package/packages/scheduler-adapter/dist/interface.js.map +1 -0
- package/packages/scheduler-adapter/dist/internal.d.ts +30 -0
- package/packages/scheduler-adapter/dist/internal.d.ts.map +1 -0
- package/packages/scheduler-adapter/dist/internal.js +82 -0
- package/packages/scheduler-adapter/dist/internal.js.map +1 -0
- package/packages/scheduler-adapter/dist/systemd.d.ts +31 -0
- package/packages/scheduler-adapter/dist/systemd.d.ts.map +1 -0
- package/packages/scheduler-adapter/dist/systemd.js +203 -0
- package/packages/scheduler-adapter/dist/systemd.js.map +1 -0
- package/packages/scheduler-adapter/package.json +32 -0
- package/packages/scheduler-adapter/src/cron.ts +123 -0
- package/packages/scheduler-adapter/src/index.ts +9 -0
- package/packages/scheduler-adapter/src/interface.ts +61 -0
- package/packages/scheduler-adapter/src/internal.ts +99 -0
- package/packages/scheduler-adapter/src/systemd.ts +221 -0
- package/packages/scheduler-adapter/tsconfig.json +22 -0
- package/packages/scheduler-adapter/tsconfig.tsbuildinfo +1 -0
- package/packages/types/dist/ai-providers.d.ts +0 -36
- package/packages/types/dist/ai-providers.d.ts.map +0 -1
- package/packages/types/dist/ai-providers.js +0 -66
- package/packages/types/dist/ai-providers.js.map +0 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Stub — RFC-0101 §4
|
|
3
|
+
*
|
|
4
|
+
* A worker is a pi session that owns tasks from the inbox.
|
|
5
|
+
* This is the lightweight worker-side of the lease protocol.
|
|
6
|
+
*
|
|
7
|
+
* The worker picks up work via the inbox's `claim()` method
|
|
8
|
+
* and reports results via `complete()`.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Worker — picks up tasks from the inbox and processes them.
|
|
12
|
+
*
|
|
13
|
+
* Lifecycle:
|
|
14
|
+
* 1. Construct with inbox + workerId
|
|
15
|
+
* 2. `start()` — begins polling/claiming tasks
|
|
16
|
+
* 3. `stop()` — stops claiming, expires held leases
|
|
17
|
+
*
|
|
18
|
+
* The worker does NOT run tasks itself — it delegates to the harness
|
|
19
|
+
* loop runtime. It just manages the lease lifecycle.
|
|
20
|
+
*/
|
|
21
|
+
export class Worker {
|
|
22
|
+
config;
|
|
23
|
+
events = {};
|
|
24
|
+
running = false;
|
|
25
|
+
heartbeatTimer = null;
|
|
26
|
+
constructor(config) {
|
|
27
|
+
this.config = {
|
|
28
|
+
heartbeatIntervalMs: 30_000,
|
|
29
|
+
...config,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
on(event, handler) {
|
|
33
|
+
this.events[event] = handler;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Start the worker — begins polling for available tasks.
|
|
37
|
+
*/
|
|
38
|
+
async start() {
|
|
39
|
+
if (this.running)
|
|
40
|
+
return;
|
|
41
|
+
this.running = true;
|
|
42
|
+
// Start heartbeat
|
|
43
|
+
this.heartbeatTimer = setInterval(() => this._heartbeat(), this.config.heartbeatIntervalMs);
|
|
44
|
+
// Claim any immediately available tasks
|
|
45
|
+
await this._poll();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Stop the worker — stops polling and expires any held leases.
|
|
49
|
+
*/
|
|
50
|
+
async stop() {
|
|
51
|
+
if (!this.running)
|
|
52
|
+
return;
|
|
53
|
+
this.running = false;
|
|
54
|
+
if (this.heartbeatTimer) {
|
|
55
|
+
clearInterval(this.heartbeatTimer);
|
|
56
|
+
this.heartbeatTimer = null;
|
|
57
|
+
}
|
|
58
|
+
// Expire all leases held by this worker
|
|
59
|
+
// (inbox.expireLease is idempotent — safe to call repeatedly)
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Status of this worker.
|
|
63
|
+
*/
|
|
64
|
+
status() {
|
|
65
|
+
return {
|
|
66
|
+
workerId: this.config.workerId,
|
|
67
|
+
running: this.running,
|
|
68
|
+
heartbeatIntervalMs: this.config.heartbeatIntervalMs ?? 30_000,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async _poll() {
|
|
72
|
+
if (!this.running)
|
|
73
|
+
return;
|
|
74
|
+
try {
|
|
75
|
+
// Try to claim one task
|
|
76
|
+
const lease = await this.config.inbox.claim({
|
|
77
|
+
workerId: this.config.workerId,
|
|
78
|
+
heartbeatIntervalMs: this.config.heartbeatIntervalMs ?? 30_000,
|
|
79
|
+
});
|
|
80
|
+
if (lease) {
|
|
81
|
+
this.events.onLease?.(lease);
|
|
82
|
+
await this._processTask(lease);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
this.config.onError?.("__poll__", err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async _processTask(lease) {
|
|
90
|
+
const task = lease.task;
|
|
91
|
+
try {
|
|
92
|
+
// Delegate to harness loop runtime (stub — will be wired in Phase 2)
|
|
93
|
+
const result = await this._runTask(task);
|
|
94
|
+
// Report completion
|
|
95
|
+
await this.config.inbox.complete(lease.taskId, result);
|
|
96
|
+
this.events.onComplete?.(lease.taskId, result);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
this.events.onError?.(lease.taskId, err);
|
|
100
|
+
await this.config.inbox.complete(lease.taskId, {
|
|
101
|
+
ok: false,
|
|
102
|
+
error: String(err),
|
|
103
|
+
completedAt: new Date().toISOString(),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// Continue polling for more work
|
|
107
|
+
if (this.running)
|
|
108
|
+
void this._poll();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Run a single task. Stub — replaces with actual harness loop integration.
|
|
112
|
+
*/
|
|
113
|
+
async _runTask(task) {
|
|
114
|
+
// TODO(Phase 2): Wire to harness loop runtime
|
|
115
|
+
// For now, return a stub result so the lease protocol works end-to-end.
|
|
116
|
+
return {
|
|
117
|
+
ok: true,
|
|
118
|
+
summary: `[stub] Processed ${task.id}`,
|
|
119
|
+
completedAt: new Date().toISOString(),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async _heartbeat() {
|
|
123
|
+
// Refresh leases held by this worker
|
|
124
|
+
// The inbox tracks which leases this worker holds and refreshes them.
|
|
125
|
+
try {
|
|
126
|
+
await this.config.inbox.refreshLeases(this.config.workerId);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Non-fatal — lease will expire naturally
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pi/autonomous-runtime",
|
|
3
|
+
"version": "0.9.15",
|
|
4
|
+
"description": "Autonomous Operations Runtime — task inbox, lease management, and worker lifecycle (RFC-0101)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"test": "bun test",
|
|
17
|
+
"test:watch": "bun test --watch",
|
|
18
|
+
"clean": "rm -rf dist"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"autonomous",
|
|
22
|
+
"runtime",
|
|
23
|
+
"task-queue",
|
|
24
|
+
"lease",
|
|
25
|
+
"worker",
|
|
26
|
+
"rfc-0101"
|
|
27
|
+
],
|
|
28
|
+
"author": "",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^20.11.0",
|
|
32
|
+
"typescript": "^7.0.2"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Inbox — RFC-0101 §2
|
|
3
|
+
*
|
|
4
|
+
* Append-only durable store for TaskRecords backed by `tasks.jsonl`.
|
|
5
|
+
* One JSONL line per task — easy to tail, grep, and replay.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
appendFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { dirname } from "node:path";
|
|
15
|
+
import type { TaskRecord, TaskStatus, TaskEvent } from "./types.js";
|
|
16
|
+
import { getInboxDir, getTasksPath } from "./types.js";
|
|
17
|
+
|
|
18
|
+
// ─── Errors ───────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export class InboxError extends Error {
|
|
21
|
+
constructor(msg: string) {
|
|
22
|
+
super(`[InboxError] ${msg}`);
|
|
23
|
+
this.name = "InboxError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── TaskInbox ────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Manages the append-only task inbox at `~/.pi/harness/inbox/tasks.jsonl`.
|
|
31
|
+
*
|
|
32
|
+
* Storage layout:
|
|
33
|
+
* ```
|
|
34
|
+
* ~/.pi/harness/inbox/
|
|
35
|
+
* ├── tasks.jsonl # append-only log (one JSON line per task)
|
|
36
|
+
* ├── claimed/ # active leases (one JSON file per task)
|
|
37
|
+
* └── BACKLOG.md # human-friendly authoring surface (optional)
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* Operations are:
|
|
41
|
+
* - **append** — append a new task to tasks.jsonl
|
|
42
|
+
* - **list** — read all tasks, filterable by status
|
|
43
|
+
* - **transition** — update a task's status in-place (rewrite)
|
|
44
|
+
* - **get** — read one task by id
|
|
45
|
+
* - **reindex** — compact tasks.jsonl by rewriting it from in-memory state
|
|
46
|
+
*/
|
|
47
|
+
export class TaskInbox {
|
|
48
|
+
private readonly tasksPath: string;
|
|
49
|
+
private readonly inboxDir: string;
|
|
50
|
+
private _cache: TaskRecord[] | null = null;
|
|
51
|
+
|
|
52
|
+
constructor(options: { tasksPath?: string; inboxDir?: string } = {}) {
|
|
53
|
+
this.inboxDir = options.inboxDir ?? getInboxDir();
|
|
54
|
+
this.tasksPath = options.tasksPath ?? getTasksPath();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Ensure the inbox directory exists. Call before any write. */
|
|
58
|
+
ensureDir(): void {
|
|
59
|
+
if (!existsSync(this.inboxDir)) {
|
|
60
|
+
mkdirSync(this.inboxDir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── append ──────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Append a new task to tasks.jsonl.
|
|
68
|
+
* The task is validated before writing. Duplicate ids are rejected.
|
|
69
|
+
*
|
|
70
|
+
* @throws {InboxError} if the task id already exists or validation fails
|
|
71
|
+
*/
|
|
72
|
+
append(task: TaskRecord): void {
|
|
73
|
+
this.ensureDir();
|
|
74
|
+
|
|
75
|
+
// Guard against duplicate ids
|
|
76
|
+
const existing = this.list().find((t) => t.id === task.id);
|
|
77
|
+
if (existing) {
|
|
78
|
+
throw new InboxError(
|
|
79
|
+
`Task ${task.id} already exists in inbox (status=${existing.status})`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const line = JSON.stringify(task) + "\n";
|
|
84
|
+
appendFileSync(this.tasksPath, line, "utf8");
|
|
85
|
+
this._cache = null; // invalidate
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── list ─────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Read all tasks from tasks.jsonl.
|
|
92
|
+
* Results are sorted by priority ASC then createdAt ASC (oldest first).
|
|
93
|
+
*
|
|
94
|
+
* @param filter Optionally filter by status. If omitted, returns all.
|
|
95
|
+
*/
|
|
96
|
+
list(filter?: { status?: TaskStatus | TaskStatus[] }): TaskRecord[] {
|
|
97
|
+
if (!existsSync(this.tasksPath)) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const raw = readFileSync(this.tasksPath, "utf8");
|
|
102
|
+
const tasks: TaskRecord[] = [];
|
|
103
|
+
|
|
104
|
+
for (const line of raw.split("\n")) {
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
if (!trimmed) continue;
|
|
107
|
+
try {
|
|
108
|
+
const task = JSON.parse(trimmed) as TaskRecord;
|
|
109
|
+
tasks.push(task);
|
|
110
|
+
} catch {
|
|
111
|
+
// Corrupt line — skip but don't fail the whole list
|
|
112
|
+
console.warn(`[TaskInbox] Skipping corrupt line in ${this.tasksPath}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Filter by status
|
|
117
|
+
let filtered = tasks;
|
|
118
|
+
if (filter?.status !== undefined) {
|
|
119
|
+
const statuses = Array.isArray(filter.status)
|
|
120
|
+
? filter.status
|
|
121
|
+
: [filter.status];
|
|
122
|
+
filtered = tasks.filter((t) => statuses.includes(t.status));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Sort: priority ASC, then createdAt ASC
|
|
126
|
+
filtered.sort((a, b) => {
|
|
127
|
+
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
128
|
+
return a.createdAt.localeCompare(b.createdAt);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return filtered;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── get ─────────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
/** Retrieve a single task by id, or null if not found. */
|
|
137
|
+
get(id: string): TaskRecord | null {
|
|
138
|
+
const tasks = this.list();
|
|
139
|
+
return tasks.find((t) => t.id === id) ?? null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── transition ──────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Update a task's status in-place inside tasks.jsonl.
|
|
146
|
+
*
|
|
147
|
+
* Uses read→parse→update→rewrite to avoid corrupting the JSONL file.
|
|
148
|
+
* The rewrite is atomic (write-tmp-then-rename).
|
|
149
|
+
*
|
|
150
|
+
* @param taskId The id of the task to transition
|
|
151
|
+
* @param newStatus The target status
|
|
152
|
+
* @param extra Optional additional fields to merge into the task record
|
|
153
|
+
* @param event Optional TaskEvent to append to the history
|
|
154
|
+
* @throws {InboxError} if the task does not exist
|
|
155
|
+
*/
|
|
156
|
+
transition(
|
|
157
|
+
taskId: string,
|
|
158
|
+
newStatus: TaskStatus,
|
|
159
|
+
extra?: Partial<TaskRecord>,
|
|
160
|
+
event?: TaskEvent,
|
|
161
|
+
): TaskRecord {
|
|
162
|
+
if (!existsSync(this.tasksPath)) {
|
|
163
|
+
throw new InboxError(`tasks.jsonl does not exist at ${this.tasksPath}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const tasks = this.list();
|
|
167
|
+
const idx = tasks.findIndex((t) => t.id === taskId);
|
|
168
|
+
if (idx === -1) {
|
|
169
|
+
throw new InboxError(`Task ${taskId} not found in inbox`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const updated: TaskRecord = {
|
|
173
|
+
...tasks[idx],
|
|
174
|
+
...extra,
|
|
175
|
+
status: newStatus,
|
|
176
|
+
updatedAt: new Date().toISOString(),
|
|
177
|
+
history: event
|
|
178
|
+
? [...tasks[idx].history, event]
|
|
179
|
+
: [
|
|
180
|
+
...tasks[idx].history,
|
|
181
|
+
{
|
|
182
|
+
ts: new Date().toISOString(),
|
|
183
|
+
kind: "transitioned",
|
|
184
|
+
payload: { from: tasks[idx].status, to: newStatus },
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Rewrite tasks.jsonl atomically
|
|
190
|
+
this._rewriteAll([
|
|
191
|
+
...tasks.slice(0, idx),
|
|
192
|
+
updated,
|
|
193
|
+
...tasks.slice(idx + 1),
|
|
194
|
+
]);
|
|
195
|
+
this._cache = null;
|
|
196
|
+
|
|
197
|
+
return updated;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─── markDone / markFailed ───────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
/** Convenience: mark a task as completed with a result. */
|
|
203
|
+
complete(taskId: string, result: TaskRecord["result"]): TaskRecord {
|
|
204
|
+
return this.transition(
|
|
205
|
+
taskId,
|
|
206
|
+
"completed",
|
|
207
|
+
{ result },
|
|
208
|
+
{
|
|
209
|
+
ts: new Date().toISOString(),
|
|
210
|
+
kind: "completed",
|
|
211
|
+
payload: { result },
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Convenience: mark a task as failed with a reason. */
|
|
217
|
+
fail(taskId: string, reason: string): TaskRecord {
|
|
218
|
+
const task = this.get(taskId);
|
|
219
|
+
const attempts = (task?.attempts ?? 0) + 1;
|
|
220
|
+
const newStatus =
|
|
221
|
+
attempts >= (task?.maxAttempts ?? 3) ? "dead_letter" : "retrying";
|
|
222
|
+
return this.transition(
|
|
223
|
+
taskId,
|
|
224
|
+
newStatus,
|
|
225
|
+
{ failureReason: reason, attempts },
|
|
226
|
+
{
|
|
227
|
+
ts: new Date().toISOString(),
|
|
228
|
+
kind: newStatus === "dead_letter" ? "dead_lettered" : "failed",
|
|
229
|
+
payload: { reason, attempts },
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ─── reindex ─────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
/** Rewrite tasks.jsonl from the given task list. */
|
|
237
|
+
private _rewriteAll(tasks: TaskRecord[]): void {
|
|
238
|
+
const tmp = `${this.tasksPath}.tmp`;
|
|
239
|
+
const content = tasks.map((t) => JSON.stringify(t)).join("\n") + "\n";
|
|
240
|
+
writeFileSync(tmp, content, "utf8");
|
|
241
|
+
// Atomic rename (POSIX guarantees atomicity)
|
|
242
|
+
// Bun/Node will use the native rename which is atomic on POSIX
|
|
243
|
+
const { renameSync } = require("node:fs");
|
|
244
|
+
renameSync(tmp, this.tasksPath);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Force a full reindex. Call after manual edits to tasks.jsonl. */
|
|
248
|
+
reindex(): void {
|
|
249
|
+
if (!existsSync(this.tasksPath)) return;
|
|
250
|
+
const tasks = this.list(); // reads and validates
|
|
251
|
+
this._rewriteAll(tasks);
|
|
252
|
+
this._cache = null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── count ───────────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
/** Return counts of tasks grouped by status. */
|
|
258
|
+
stats(): Record<TaskStatus, number> {
|
|
259
|
+
const tasks = this.list();
|
|
260
|
+
const counts: Record<string, number> = {};
|
|
261
|
+
for (const t of tasks) {
|
|
262
|
+
counts[t.status] = (counts[t.status] ?? 0) + 1;
|
|
263
|
+
}
|
|
264
|
+
return counts as Record<TaskStatus, number>;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autonomous Operations Runtime — Public API
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 exports (RFC-0101 §1-2):
|
|
5
|
+
* - TaskRecord, TaskLease, TaskEvent, TaskStatus, CapabilityName, ApprovalClass types
|
|
6
|
+
* - TaskInbox: append, list, get, transition, complete, fail
|
|
7
|
+
* - LeaseManager: claim, heartbeat, release, reap, recoverOnStartup
|
|
8
|
+
*
|
|
9
|
+
* Phase 2+ exports (added as implemented):
|
|
10
|
+
* - Worker lifecycle (worker.ts)
|
|
11
|
+
* - Privilege broker (privilege-broker package)
|
|
12
|
+
* - Scheduler adapter (scheduler-adapter package)
|
|
13
|
+
*/
|
|
14
|
+
export * from "./types.js";
|
|
15
|
+
export { TaskInbox, InboxError } from "./inbox.js";
|
|
16
|
+
export {
|
|
17
|
+
LeaseManager,
|
|
18
|
+
LeaseError,
|
|
19
|
+
DEFAULT_LEASE_TTL_MS,
|
|
20
|
+
HEARTBEAT_INTERVAL_MS,
|
|
21
|
+
HEARTBEAT_EXTENSION_MS,
|
|
22
|
+
} from "./lease.js";
|