@ixo/oracle-runtime-workers 0.4.1 → 0.9.0
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/README.md +5 -1
- package/package.json +2 -2
- package/src/attachments/download.ts +43 -22
- package/src/attachments/index.ts +1 -0
- package/src/core/env.ts +19 -0
- package/src/core/main-agent.ts +56 -4
- package/src/core/middlewares/dangling-tool-calls.ts +99 -0
- package/src/core/middlewares/index.ts +5 -0
- package/src/core/runtime-context.ts +14 -5
- package/src/core/subagent-as-tool.ts +12 -1
- package/src/do/ambient.ts +13 -4
- package/src/do/boot-dirty.ts +38 -0
- package/src/do/contracts.ts +91 -18
- package/src/do/matrix-turn-ledger.ts +126 -0
- package/src/do/reauth-prompt.ts +76 -0
- package/src/do/room-mirror.ts +111 -0
- package/src/do/sse-stream.ts +104 -2
- package/src/do/transcript.ts +15 -2
- package/src/do/turn-config.ts +24 -0
- package/src/do/user-oracle-do.ts +339 -67
- package/src/matrix/gateway-do.ts +233 -42
- package/src/matrix/inbox-store.ts +188 -0
- package/src/matrix/txn-id.ts +35 -0
- package/src/matrix/work-status.ts +15 -3
- package/src/owner-store/matrix-media-store.ts +59 -39
- package/src/owner-store/migrating-store.ts +22 -33
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/types.ts +25 -1
- package/src/plugins/portal/action-log.ts +36 -9
- package/src/plugins/tasks/tasks-tools.ts +7 -7
- package/src/shell/app.ts +80 -7
- package/src/shell/turn-body-cap.ts +19 -0
- package/src/tasks/schedule-input.ts +102 -0
- package/src/tasks/scheduler.ts +351 -54
- package/src/tasks/store.ts +198 -3
package/src/tasks/store.ts
CHANGED
|
@@ -55,6 +55,23 @@ export function pendingApprovalOf(
|
|
|
55
55
|
return undefined;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Where a fired run is in its life. Written before the agent turn starts
|
|
60
|
+
* (`running`), when the result is stored and the schedule advanced
|
|
61
|
+
* (`delivering`), and when the room has acknowledged the message
|
|
62
|
+
* (`delivered`). A row a later alarm finds in `running` or `delivering`
|
|
63
|
+
* without a live owner belongs to an incarnation of the object that died:
|
|
64
|
+
* `delivering` is re-sent from the stored result, `running` is closed as
|
|
65
|
+
* `interrupted` and never re-run (its tools may have executed). Approval
|
|
66
|
+
* bookkeeping rows carry no state.
|
|
67
|
+
*/
|
|
68
|
+
export type TaskRunState =
|
|
69
|
+
| 'running'
|
|
70
|
+
| 'delivering'
|
|
71
|
+
| 'delivered'
|
|
72
|
+
| 'failed'
|
|
73
|
+
| 'interrupted';
|
|
74
|
+
|
|
58
75
|
/** One audit row per fired run / approval decision. */
|
|
59
76
|
export interface TaskRunEntry {
|
|
60
77
|
runId: string;
|
|
@@ -63,7 +80,29 @@ export interface TaskRunEntry {
|
|
|
63
80
|
finishedAt?: string;
|
|
64
81
|
/** true = delivered, false = failed, undefined = no run happened (approval bookkeeping). */
|
|
65
82
|
ok?: boolean;
|
|
83
|
+
/** Technical reason / summary — for operators and the run history, never shown to the user as-is. */
|
|
66
84
|
detail?: string;
|
|
85
|
+
state?: TaskRunState;
|
|
86
|
+
/** Delivery rounds attempted so far (a round is one retried send sequence). */
|
|
87
|
+
attempts?: number;
|
|
88
|
+
/** Earliest time the next delivery round may start (ms epoch). */
|
|
89
|
+
retryAt?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A run a previous or current incarnation has not finished: what re-delivery or closure needs. */
|
|
93
|
+
export interface OpenTaskRun {
|
|
94
|
+
runId: string;
|
|
95
|
+
taskId: string;
|
|
96
|
+
startedAt: string;
|
|
97
|
+
state: 'running' | 'delivering';
|
|
98
|
+
/** Transaction id every send of this run's result uses (server-side dedupe). */
|
|
99
|
+
txnId: string;
|
|
100
|
+
/** Where the result goes; resolved before the turn, so a re-delivery needs no gateway lookup. */
|
|
101
|
+
roomId?: string;
|
|
102
|
+
/** The finished result, once the run reached `delivering`. */
|
|
103
|
+
resultText?: string;
|
|
104
|
+
attempts: number;
|
|
105
|
+
retryAt?: number;
|
|
67
106
|
}
|
|
68
107
|
|
|
69
108
|
type TaskRow = {
|
|
@@ -90,8 +129,33 @@ type RunRow = {
|
|
|
90
129
|
finished_at: string | null;
|
|
91
130
|
ok: number | null;
|
|
92
131
|
detail: string | null;
|
|
132
|
+
state: string | null;
|
|
133
|
+
attempts: number | null;
|
|
134
|
+
retry_at: number | bigint | null;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
type OpenRunRow = {
|
|
138
|
+
run_id: string;
|
|
139
|
+
task_id: string;
|
|
140
|
+
started_at: string;
|
|
141
|
+
state: string;
|
|
142
|
+
txn_id: string | null;
|
|
143
|
+
room_id: string | null;
|
|
144
|
+
result_text: string | null;
|
|
145
|
+
attempts: number | null;
|
|
146
|
+
retry_at: number | bigint | null;
|
|
93
147
|
};
|
|
94
148
|
|
|
149
|
+
/** Columns added after the first release; added to existing files on setup. */
|
|
150
|
+
const RUN_COLUMN_UPGRADES: ReadonlyArray<readonly [string, string]> = [
|
|
151
|
+
['state', 'TEXT'],
|
|
152
|
+
['txn_id', 'TEXT'],
|
|
153
|
+
['room_id', 'TEXT'],
|
|
154
|
+
['result_text', 'TEXT'],
|
|
155
|
+
['attempts', 'INTEGER NOT NULL DEFAULT 0'],
|
|
156
|
+
['retry_at', 'INTEGER'],
|
|
157
|
+
];
|
|
158
|
+
|
|
95
159
|
const TASK_COLUMNS = `id, title, spec, schedule_json, status, approval, created_at,
|
|
96
160
|
updated_at, next_run_at, last_run_at, last_result_json, consecutive_failures, pending_approval_at,
|
|
97
161
|
delivery_room_id`;
|
|
@@ -237,6 +301,23 @@ export class TasksStore {
|
|
|
237
301
|
await this.db.run(
|
|
238
302
|
`CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs(task_id, started_at)`,
|
|
239
303
|
);
|
|
304
|
+
// Files written before the run ledger existed lack these columns. One
|
|
305
|
+
// metadata read per object boot; the ALTERs run once per file, ever.
|
|
306
|
+
const present = new Set(
|
|
307
|
+
(
|
|
308
|
+
await this.db.exec<{ name: string }>(`PRAGMA table_info(task_runs)`)
|
|
309
|
+
).map((row) => row.name),
|
|
310
|
+
);
|
|
311
|
+
for (const [name, ddl] of RUN_COLUMN_UPGRADES) {
|
|
312
|
+
if (!present.has(name))
|
|
313
|
+
await this.db.run(`ALTER TABLE task_runs ADD COLUMN ${name} ${ddl}`);
|
|
314
|
+
}
|
|
315
|
+
// The open-run scan on every alarm touches only the (normally empty)
|
|
316
|
+
// set of unfinished rows, never the whole audit trail.
|
|
317
|
+
await this.db.run(
|
|
318
|
+
`CREATE INDEX IF NOT EXISTS idx_task_runs_open ON task_runs(state)
|
|
319
|
+
WHERE state IN ('running', 'delivering')`,
|
|
320
|
+
);
|
|
240
321
|
}
|
|
241
322
|
|
|
242
323
|
async insert(record: TaskRecord): Promise<void> {
|
|
@@ -315,8 +396,8 @@ export class TasksStore {
|
|
|
315
396
|
async recordRun(entry: TaskRunEntry): Promise<void> {
|
|
316
397
|
await this.setup();
|
|
317
398
|
await this.db.run(
|
|
318
|
-
`INSERT INTO task_runs (run_id, task_id, started_at, finished_at, ok, detail)
|
|
319
|
-
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
399
|
+
`INSERT INTO task_runs (run_id, task_id, started_at, finished_at, ok, detail, state, attempts, retry_at)
|
|
400
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
320
401
|
[
|
|
321
402
|
entry.runId,
|
|
322
403
|
entry.taskId,
|
|
@@ -324,15 +405,126 @@ export class TasksStore {
|
|
|
324
405
|
entry.finishedAt ?? null,
|
|
325
406
|
entry.ok === undefined ? null : entry.ok,
|
|
326
407
|
entry.detail ?? null,
|
|
408
|
+
entry.state ?? null,
|
|
409
|
+
entry.attempts ?? 0,
|
|
410
|
+
entry.retryAt ?? null,
|
|
327
411
|
],
|
|
328
412
|
);
|
|
329
413
|
}
|
|
330
414
|
|
|
415
|
+
/**
|
|
416
|
+
* Open a run's ledger row before its agent turn starts — one row write.
|
|
417
|
+
* `txnId` is fixed here so every later send of the result, by this or a
|
|
418
|
+
* later incarnation, carries the same id.
|
|
419
|
+
*/
|
|
420
|
+
async startRun(run: {
|
|
421
|
+
runId: string;
|
|
422
|
+
taskId: string;
|
|
423
|
+
startedAt: string;
|
|
424
|
+
txnId: string;
|
|
425
|
+
}): Promise<void> {
|
|
426
|
+
await this.setup();
|
|
427
|
+
await this.db.run(
|
|
428
|
+
`INSERT INTO task_runs (run_id, task_id, started_at, state, txn_id, attempts)
|
|
429
|
+
VALUES (?, ?, ?, 'running', ?, 0)`,
|
|
430
|
+
[run.runId, run.taskId, run.startedAt, run.txnId],
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Move a run's row along its life (one row write; only the given fields change). */
|
|
435
|
+
async updateRun(
|
|
436
|
+
runId: string,
|
|
437
|
+
patch: {
|
|
438
|
+
state?: TaskRunState;
|
|
439
|
+
roomId?: string;
|
|
440
|
+
resultText?: string;
|
|
441
|
+
finishedAt?: string;
|
|
442
|
+
ok?: boolean;
|
|
443
|
+
detail?: string;
|
|
444
|
+
attempts?: number;
|
|
445
|
+
retryAt?: number | null;
|
|
446
|
+
},
|
|
447
|
+
): Promise<void> {
|
|
448
|
+
await this.setup();
|
|
449
|
+
const sets: string[] = [];
|
|
450
|
+
const params: Array<string | number | null> = [];
|
|
451
|
+
if (patch.state !== undefined) {
|
|
452
|
+
sets.push('state = ?');
|
|
453
|
+
params.push(patch.state);
|
|
454
|
+
}
|
|
455
|
+
if (patch.roomId !== undefined) {
|
|
456
|
+
sets.push('room_id = ?');
|
|
457
|
+
params.push(patch.roomId);
|
|
458
|
+
}
|
|
459
|
+
if (patch.resultText !== undefined) {
|
|
460
|
+
sets.push('result_text = ?');
|
|
461
|
+
params.push(patch.resultText);
|
|
462
|
+
}
|
|
463
|
+
if (patch.finishedAt !== undefined) {
|
|
464
|
+
sets.push('finished_at = ?');
|
|
465
|
+
params.push(patch.finishedAt);
|
|
466
|
+
}
|
|
467
|
+
if (patch.ok !== undefined) {
|
|
468
|
+
sets.push('ok = ?');
|
|
469
|
+
params.push(patch.ok ? 1 : 0);
|
|
470
|
+
}
|
|
471
|
+
if (patch.detail !== undefined) {
|
|
472
|
+
sets.push('detail = ?');
|
|
473
|
+
params.push(patch.detail);
|
|
474
|
+
}
|
|
475
|
+
if (patch.attempts !== undefined) {
|
|
476
|
+
sets.push('attempts = ?');
|
|
477
|
+
params.push(patch.attempts);
|
|
478
|
+
}
|
|
479
|
+
if (patch.retryAt !== undefined) {
|
|
480
|
+
sets.push('retry_at = ?');
|
|
481
|
+
params.push(patch.retryAt);
|
|
482
|
+
}
|
|
483
|
+
if (sets.length === 0) return;
|
|
484
|
+
params.push(runId);
|
|
485
|
+
await this.db.run(
|
|
486
|
+
`UPDATE task_runs SET ${sets.join(', ')} WHERE run_id = ?`,
|
|
487
|
+
params,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Runs no incarnation has finished, oldest first (normally none — an indexed read). */
|
|
492
|
+
async openRuns(): Promise<OpenTaskRun[]> {
|
|
493
|
+
await this.setup();
|
|
494
|
+
const rows = await this.db.exec<OpenRunRow>(
|
|
495
|
+
`SELECT run_id, task_id, started_at, state, txn_id, room_id, result_text, attempts, retry_at
|
|
496
|
+
FROM task_runs WHERE state IN ('running', 'delivering')
|
|
497
|
+
ORDER BY started_at, run_id`,
|
|
498
|
+
);
|
|
499
|
+
return rows.map((row) => ({
|
|
500
|
+
runId: row.run_id,
|
|
501
|
+
taskId: row.task_id,
|
|
502
|
+
startedAt: row.started_at,
|
|
503
|
+
state: row.state === 'delivering' ? 'delivering' : 'running',
|
|
504
|
+
txnId: row.txn_id ?? `task-${row.run_id}`,
|
|
505
|
+
...(row.room_id ? { roomId: row.room_id } : {}),
|
|
506
|
+
...(row.result_text !== null ? { resultText: row.result_text } : {}),
|
|
507
|
+
attempts: Number(row.attempts ?? 0),
|
|
508
|
+
...(row.retry_at !== null ? { retryAt: Number(row.retry_at) } : {}),
|
|
509
|
+
}));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Earliest pending delivery retry (ms epoch) over open runs, or null. */
|
|
513
|
+
async minRetryAt(): Promise<number | null> {
|
|
514
|
+
await this.setup();
|
|
515
|
+
const row = await this.db.get<{ next: number | bigint | null }>(
|
|
516
|
+
`SELECT MIN(retry_at) AS next FROM task_runs
|
|
517
|
+
WHERE state = 'delivering' AND retry_at IS NOT NULL`,
|
|
518
|
+
);
|
|
519
|
+
if (row === undefined || row.next === null) return null;
|
|
520
|
+
return Number(row.next);
|
|
521
|
+
}
|
|
522
|
+
|
|
331
523
|
/** Audit trail for one task, newest first. */
|
|
332
524
|
async listRuns(taskId: string, limit = 20): Promise<TaskRunEntry[]> {
|
|
333
525
|
await this.setup();
|
|
334
526
|
const rows = await this.db.exec<RunRow>(
|
|
335
|
-
`SELECT run_id, task_id, started_at, finished_at, ok, detail
|
|
527
|
+
`SELECT run_id, task_id, started_at, finished_at, ok, detail, state, attempts, retry_at
|
|
336
528
|
FROM task_runs WHERE task_id = ? ORDER BY started_at DESC, run_id DESC LIMIT ?`,
|
|
337
529
|
[taskId, limit],
|
|
338
530
|
);
|
|
@@ -345,6 +537,9 @@ export class TasksStore {
|
|
|
345
537
|
if (row.finished_at !== null) entry.finishedAt = row.finished_at;
|
|
346
538
|
if (row.ok !== null) entry.ok = row.ok !== 0;
|
|
347
539
|
if (row.detail !== null) entry.detail = row.detail;
|
|
540
|
+
if (row.state !== null) entry.state = row.state as TaskRunState;
|
|
541
|
+
if (row.attempts !== null) entry.attempts = Number(row.attempts);
|
|
542
|
+
if (row.retry_at !== null) entry.retryAt = Number(row.retry_at);
|
|
348
543
|
return entry;
|
|
349
544
|
});
|
|
350
545
|
}
|