@ixo/oracle-runtime-workers 0.2.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 +110 -0
- package/package.json +77 -0
- package/src/attachments/classify.ts +114 -0
- package/src/attachments/content-blocks.ts +51 -0
- package/src/attachments/download.ts +235 -0
- package/src/attachments/extract.ts +241 -0
- package/src/attachments/index.ts +84 -0
- package/src/attachments/magic.ts +133 -0
- package/src/attachments/pipeline.ts +409 -0
- package/src/attachments/retention.ts +205 -0
- package/src/attachments/route.ts +41 -0
- package/src/attachments/sandbox-archive.ts +90 -0
- package/src/attachments/sanitize.ts +35 -0
- package/src/attachments/types.ts +86 -0
- package/src/attachments/view.ts +107 -0
- package/src/core/env.ts +200 -0
- package/src/core/index.ts +513 -0
- package/src/core/llm.ts +842 -0
- package/src/core/main-agent-types.ts +123 -0
- package/src/core/main-agent.ts +399 -0
- package/src/core/manifest.ts +365 -0
- package/src/core/meta-tools.ts +232 -0
- package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
- package/src/core/middlewares/capability-gate.ts +76 -0
- package/src/core/middlewares/index.ts +18 -0
- package/src/core/middlewares/page-context.ts +98 -0
- package/src/core/middlewares/safety-guardrail.ts +117 -0
- package/src/core/middlewares/summarization.ts +99 -0
- package/src/core/middlewares/tool-repetition-guard.ts +125 -0
- package/src/core/middlewares/tool-validation.ts +83 -0
- package/src/core/middlewares/work-status.ts +57 -0
- package/src/core/openrouter-pricing.ts +124 -0
- package/src/core/plugin-loader.ts +192 -0
- package/src/core/plugins/index.ts +6 -0
- package/src/core/plugins/skills/index.ts +8 -0
- package/src/core/plugins/skills/skills-tools.ts +356 -0
- package/src/core/plugins/skills/skills.plugin.ts +113 -0
- package/src/core/plugins/weather/index.ts +8 -0
- package/src/core/plugins/weather/weather-client.ts +153 -0
- package/src/core/plugins/weather/weather-middleware.ts +27 -0
- package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
- package/src/core/plugins/weather/weather-tools.ts +103 -0
- package/src/core/plugins/weather/weather.plugin.ts +227 -0
- package/src/core/prompt-composer.ts +469 -0
- package/src/core/registries.ts +642 -0
- package/src/core/runtime-context.ts +619 -0
- package/src/core/state.ts +108 -0
- package/src/core/sub-agent-fallback.ts +187 -0
- package/src/core/subagent-as-tool.ts +322 -0
- package/src/core/template.ts +245 -0
- package/src/core/utils.ts +246 -0
- package/src/core/wrap-plugin-tool.ts +100 -0
- package/src/do/ambient.ts +266 -0
- package/src/do/contracts.ts +562 -0
- package/src/do/debug-timers.ts +120 -0
- package/src/do/fresh-stub.ts +32 -0
- package/src/do/gateway-retry.ts +73 -0
- package/src/do/secrets-adapter.ts +32 -0
- package/src/do/size-alerts.ts +76 -0
- package/src/do/sse-stream.ts +472 -0
- package/src/do/transcript.ts +192 -0
- package/src/do/turn-body.ts +84 -0
- package/src/do/turn-metadata.ts +146 -0
- package/src/do/ucan-service.ts +339 -0
- package/src/do/user-oracle-do.ts +3035 -0
- package/src/gateway-worker.ts +82 -0
- package/src/index.ts +237 -0
- package/src/llm/byo-adapter.ts +59 -0
- package/src/llm/byo-catalog.ts +520 -0
- package/src/llm/byo-client.ts +280 -0
- package/src/llm/byo-routes.ts +255 -0
- package/src/llm/byo-service.ts +836 -0
- package/src/llm/chatgpt-oauth.ts +430 -0
- package/src/llm/provider-error.ts +283 -0
- package/src/matrix/gateway-do.ts +918 -0
- package/src/matrix/ingest.ts +209 -0
- package/src/matrix/replay-format.ts +34 -0
- package/src/matrix/reply-chain.ts +109 -0
- package/src/matrix/room-state-codec.ts +115 -0
- package/src/matrix/user-homeserver.ts +106 -0
- package/src/matrix/work-status.ts +294 -0
- package/src/memory/session-history-indexer.ts +328 -0
- package/src/owner-store/ixo-vfs-store.ts +570 -0
- package/src/owner-store/matrix-media-store.ts +136 -0
- package/src/owner-store/migrating-store.ts +185 -0
- package/src/owner-store/owner-copy-errors.ts +175 -0
- package/src/owner-store/retry.ts +45 -0
- package/src/owner-store/tus-upload.ts +333 -0
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/define-plugin.ts +45 -0
- package/src/plugin-api/index.ts +56 -0
- package/src/plugin-api/oracle-plugin.ts +145 -0
- package/src/plugin-api/tool-helper.ts +82 -0
- package/src/plugin-api/types.ts +816 -0
- package/src/plugin-api/ucan-errors.ts +16 -0
- package/src/plugins/agui/agui-agent.ts +115 -0
- package/src/plugins/agui/agui.plugin.ts +148 -0
- package/src/plugins/agui/index.ts +2 -0
- package/src/plugins/attachments/attachments.plugin.ts +54 -0
- package/src/plugins/attachments/index.ts +6 -0
- package/src/plugins/attachments/view-attachment-tool.ts +91 -0
- package/src/plugins/base64.ts +26 -0
- package/src/plugins/composio/composio-tools.ts +365 -0
- package/src/plugins/composio/composio-ucan.ts +43 -0
- package/src/plugins/composio/composio.plugin.ts +214 -0
- package/src/plugins/composio/index.ts +15 -0
- package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
- package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
- package/src/plugins/domain-indexer/index.ts +3 -0
- package/src/plugins/editor/blocknote-bridge.ts +120 -0
- package/src/plugins/editor/content-session.ts +331 -0
- package/src/plugins/editor/content-tools.ts +686 -0
- package/src/plugins/editor/document-model.ts +0 -0
- package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
- package/src/plugins/editor/editor-agent.ts +34 -0
- package/src/plugins/editor/editor-config.ts +64 -0
- package/src/plugins/editor/editor-mx.ts +128 -0
- package/src/plugins/editor/editor.plugin.ts +143 -0
- package/src/plugins/editor/failures.ts +154 -0
- package/src/plugins/editor/index.ts +31 -0
- package/src/plugins/editor/jsdom-shim.ts +55 -0
- package/src/plugins/editor/prompts.ts +59 -0
- package/src/plugins/editor/prop-policy.ts +147 -0
- package/src/plugins/editor/provider.ts +386 -0
- package/src/plugins/editor/room-membership.ts +95 -0
- package/src/plugins/editor/standalone-editor-tool.ts +200 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
- package/src/plugins/firecrawl/index.ts +15 -0
- package/src/plugins/flows/action-metadata.ts +3505 -0
- package/src/plugins/flows/actions.ts +190 -0
- package/src/plugins/flows/edit.ts +314 -0
- package/src/plugins/flows/errors.ts +38 -0
- package/src/plugins/flows/explain.ts +43 -0
- package/src/plugins/flows/flow-doc.ts +111 -0
- package/src/plugins/flows/flows.plugin.ts +129 -0
- package/src/plugins/flows/forms.ts +330 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/linkage.ts +147 -0
- package/src/plugins/flows/port-types.ts +53 -0
- package/src/plugins/flows/prompts.ts +65 -0
- package/src/plugins/flows/read.ts +392 -0
- package/src/plugins/flows/references.ts +66 -0
- package/src/plugins/flows/test-support.ts +165 -0
- package/src/plugins/flows/tools/authoring.ts +516 -0
- package/src/plugins/flows/tools/discovery.ts +119 -0
- package/src/plugins/flows/tools/forms.ts +142 -0
- package/src/plugins/flows/tools/inspect.ts +165 -0
- package/src/plugins/flows/tools/linkage.ts +117 -0
- package/src/plugins/flows/tools/settings.ts +184 -0
- package/src/plugins/flows/translator.ts +328 -0
- package/src/plugins/flows/types.ts +210 -0
- package/src/plugins/flows/ydoc-helpers.ts +543 -0
- package/src/plugins/index.ts +70 -0
- package/src/plugins/mcp-call-timeout.ts +38 -0
- package/src/plugins/mcp-tool-adapter.ts +87 -0
- package/src/plugins/memory/index.ts +17 -0
- package/src/plugins/memory/memory-tools.ts +463 -0
- package/src/plugins/memory/memory-ucan.ts +51 -0
- package/src/plugins/memory/memory.plugin.ts +122 -0
- package/src/plugins/memory/types.ts +147 -0
- package/src/plugins/portal/action-log.ts +34 -0
- package/src/plugins/portal/index.ts +2 -0
- package/src/plugins/portal/portal.plugin.ts +203 -0
- package/src/plugins/sandbox/index.ts +34 -0
- package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
- package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
- package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
- package/src/plugins/tasks/index.ts +17 -0
- package/src/plugins/tasks/manifest.ts +81 -0
- package/src/plugins/tasks/middleware.ts +219 -0
- package/src/plugins/tasks/tasks-tools.ts +414 -0
- package/src/plugins/tasks/tasks.plugin.ts +74 -0
- package/src/plugins/ucan-failure.ts +133 -0
- package/src/plugins/user-preferences/index.ts +22 -0
- package/src/plugins/user-preferences/schema.ts +31 -0
- package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
- package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
- package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
- package/src/plugins/vfs/index.ts +40 -0
- package/src/plugins/vfs/vfs-auth.ts +56 -0
- package/src/plugins/vfs/vfs-client.ts +655 -0
- package/src/plugins/vfs/vfs-content.ts +181 -0
- package/src/plugins/vfs/vfs-errors.ts +163 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
- package/src/plugins/vfs/vfs-tools.ts +635 -0
- package/src/plugins/vfs/vfs.plugin.ts +201 -0
- package/src/realtime/frontend-call-registry.ts +163 -0
- package/src/realtime/realtime-endpoint.ts +619 -0
- package/src/realtime/session-socket-hub.ts +249 -0
- package/src/realtime/socket-io-codec.ts +187 -0
- package/src/secrets/jwe.ts +74 -0
- package/src/secrets/pin-cipher.ts +43 -0
- package/src/secrets/secrets-service.ts +319 -0
- package/src/shell/app.ts +564 -0
- package/src/shell/auth.ts +290 -0
- package/src/sqlite/blob-codec.ts +100 -0
- package/src/sqlite/blob-compactor.ts +146 -0
- package/src/sqlite/cache-config.ts +45 -0
- package/src/sqlite/database.ts +643 -0
- package/src/sqlite/do-vfs.ts +1485 -0
- package/src/sqlite/index.ts +61 -0
- package/src/sqlite/serialization.ts +370 -0
- package/src/sqlite/sessions-store.ts +311 -0
- package/src/sqlite/sqlite-saver.ts +919 -0
- package/src/sqlite/vacuum-policy.ts +77 -0
- package/src/sqlite/wa-sqlite-loader.ts +123 -0
- package/src/sqlite/wasm.d.ts +10 -0
- package/src/tasks/schedule.ts +174 -0
- package/src/tasks/scheduler.ts +753 -0
- package/src/tasks/spec.ts +125 -0
- package/src/tasks/store.ts +351 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task spec markdown — the user-readable artifact each task is stored as.
|
|
3
|
+
*
|
|
4
|
+
* A spec is YAML frontmatter (identity + trigger + lifecycle) over a markdown
|
|
5
|
+
* body (the intent — what the agent should do each run), rendered and parsed
|
|
6
|
+
* with gray-matter exactly like the Node runtime's `spec.md` files. The
|
|
7
|
+
* Workers store keeps the spec as a column in the user's own SQLite file next
|
|
8
|
+
* to the queryable index columns, so the owner file stays self-describing and
|
|
9
|
+
* portable.
|
|
10
|
+
*/
|
|
11
|
+
import matter from 'gray-matter';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import type { OracleTaskRecord, OracleTaskSchedule } from '../plugin-api/types';
|
|
14
|
+
|
|
15
|
+
/** Self-describing task id: `task_<title-slug>_<8 hex>`. */
|
|
16
|
+
export const TASK_ID_PATTERN = /^task_[a-z0-9][a-z0-9-]*_[a-f0-9]{8}$/;
|
|
17
|
+
|
|
18
|
+
export function newTaskId(title: string): string {
|
|
19
|
+
const slug =
|
|
20
|
+
title
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
23
|
+
.replace(/^-+|-+$/g, '')
|
|
24
|
+
.slice(0, 24) || 'untitled';
|
|
25
|
+
const bytes = new Uint8Array(4);
|
|
26
|
+
crypto.getRandomValues(bytes);
|
|
27
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(
|
|
28
|
+
'',
|
|
29
|
+
);
|
|
30
|
+
return `task_${slug}_${hex}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Zod schema for `OracleTaskSchedule` — shared by the spec frontmatter, the
|
|
35
|
+
* store's `schedule_json` column and the plugin's tool inputs so all three
|
|
36
|
+
* accept exactly the same shape.
|
|
37
|
+
*/
|
|
38
|
+
export const TaskScheduleSchema = z.discriminatedUnion('kind', [
|
|
39
|
+
z
|
|
40
|
+
.object({
|
|
41
|
+
kind: z.literal('once'),
|
|
42
|
+
at: z.string().min(1),
|
|
43
|
+
})
|
|
44
|
+
.describe(
|
|
45
|
+
'A single future moment (ISO timestamp). ALWAYS use this for one-time requests — "in 10 minutes", "tomorrow at 5pm" — computing `at` from the current time. Never express a one-time request as cron.',
|
|
46
|
+
),
|
|
47
|
+
z
|
|
48
|
+
.object({
|
|
49
|
+
kind: z.literal('cron'),
|
|
50
|
+
cron: z.string().min(1),
|
|
51
|
+
timezone: z.string().min(1).optional(),
|
|
52
|
+
})
|
|
53
|
+
.describe(
|
|
54
|
+
'A repeating cron schedule, ONLY for genuinely recurring intents ("every morning at 7"). Note `*/10 * * * *` means "every 10 minutes forever", NOT "once in 10 minutes" — use kind "once" for that.',
|
|
55
|
+
),
|
|
56
|
+
z
|
|
57
|
+
.object({
|
|
58
|
+
kind: z.literal('interval'),
|
|
59
|
+
everySeconds: z.number().int().positive(),
|
|
60
|
+
})
|
|
61
|
+
.describe(
|
|
62
|
+
'Repeat every N seconds from the previous run. For simple fixed cadences that do not need cron precision.',
|
|
63
|
+
),
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
export const TASK_STATUSES = [
|
|
67
|
+
'active',
|
|
68
|
+
'paused',
|
|
69
|
+
'completed',
|
|
70
|
+
'cancelled',
|
|
71
|
+
'failed',
|
|
72
|
+
] as const;
|
|
73
|
+
|
|
74
|
+
export const TASK_APPROVALS = ['never', 'before-action'] as const;
|
|
75
|
+
|
|
76
|
+
const SpecFrontmatterSchema = z.object({
|
|
77
|
+
id: z.string().regex(TASK_ID_PATTERN),
|
|
78
|
+
title: z.string().min(1).max(120),
|
|
79
|
+
schedule: TaskScheduleSchema,
|
|
80
|
+
approval: z.enum(TASK_APPROVALS),
|
|
81
|
+
status: z.enum(TASK_STATUSES),
|
|
82
|
+
createdAt: z.string().min(1),
|
|
83
|
+
updatedAt: z.string().min(1),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export type TaskSpecFrontmatter = z.infer<typeof SpecFrontmatterSchema>;
|
|
87
|
+
|
|
88
|
+
export interface ParsedTaskSpec {
|
|
89
|
+
frontmatter: TaskSpecFrontmatter;
|
|
90
|
+
intent: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** JSON round-trip: YAML cannot serialise `undefined` values (`timezone?`). */
|
|
94
|
+
function toPlainSchedule(schedule: OracleTaskSchedule): OracleTaskSchedule {
|
|
95
|
+
return JSON.parse(JSON.stringify(schedule)) as OracleTaskSchedule;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Render a task record as its spec markdown (frontmatter + intent body). */
|
|
99
|
+
export function renderTaskSpec(record: OracleTaskRecord): string {
|
|
100
|
+
const frontmatter: TaskSpecFrontmatter = {
|
|
101
|
+
id: record.id,
|
|
102
|
+
title: record.title,
|
|
103
|
+
schedule: toPlainSchedule(record.schedule),
|
|
104
|
+
approval: record.approval,
|
|
105
|
+
status: record.status,
|
|
106
|
+
createdAt: record.createdAt,
|
|
107
|
+
updatedAt: record.updatedAt,
|
|
108
|
+
};
|
|
109
|
+
return matter.stringify(`${record.intent.trim()}\n`, frontmatter);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Parse a spec markdown back into frontmatter + intent. Throws on schema drift. */
|
|
113
|
+
export function parseTaskSpec(markdown: string): ParsedTaskSpec {
|
|
114
|
+
const parsed = matter(markdown);
|
|
115
|
+
const data: unknown = parsed.data;
|
|
116
|
+
return {
|
|
117
|
+
frontmatter: SpecFrontmatterSchema.parse(data),
|
|
118
|
+
intent: parsed.content.trim(),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The intent body of a spec without validating the frontmatter. */
|
|
123
|
+
export function specIntentOf(markdown: string): string {
|
|
124
|
+
return matter(markdown).content.trim();
|
|
125
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tasks` + `task_runs` table access in the USER'S OWN SQLite database — the
|
|
3
|
+
* Workers replacement for the Node runtime's Redis task store. Tasks live in
|
|
4
|
+
* the owner file, so exporting, pausing or deleting the file carries the
|
|
5
|
+
* tasks with it.
|
|
6
|
+
*
|
|
7
|
+
* tasks(id PK, title, spec, schedule_json, status, approval, created_at,
|
|
8
|
+
* updated_at, next_run_at, last_run_at, last_result_json,
|
|
9
|
+
* consecutive_failures, pending_approval_at)
|
|
10
|
+
* task_runs(run_id PK, task_id, started_at, finished_at, ok, detail)
|
|
11
|
+
*
|
|
12
|
+
* The `spec` column is the full markdown artifact (gray-matter frontmatter +
|
|
13
|
+
* intent body, see `spec.ts`); the sibling columns are the query index the
|
|
14
|
+
* scheduler reads (`MIN(next_run_at)`, due scans). Timestamps in `*_at` TEXT
|
|
15
|
+
* columns are ISO strings; `next_run_at` is INTEGER ms-epoch so MIN() and
|
|
16
|
+
* `<=` comparisons stay numeric. No statement binds more than 13 parameters
|
|
17
|
+
* (DO SQL caps at 100) and nothing uses LIKE.
|
|
18
|
+
*/
|
|
19
|
+
import type { DoSqliteDatabase, SqlParam } from '../sqlite/database';
|
|
20
|
+
import type {
|
|
21
|
+
OracleTaskRecord,
|
|
22
|
+
OracleTaskSchedule,
|
|
23
|
+
OracleTaskStatus,
|
|
24
|
+
} from '../plugin-api/types';
|
|
25
|
+
import {
|
|
26
|
+
renderTaskSpec,
|
|
27
|
+
specIntentOf,
|
|
28
|
+
TASK_APPROVALS,
|
|
29
|
+
TASK_STATUSES,
|
|
30
|
+
TaskScheduleSchema,
|
|
31
|
+
} from './spec';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A stored task. Extends the plugin-api record with the approval marker:
|
|
35
|
+
* a `before-action` task whose run request is waiting on the user carries
|
|
36
|
+
* `pendingApprovalAt` (ISO of when the request was posted).
|
|
37
|
+
*/
|
|
38
|
+
export interface TaskRecord extends OracleTaskRecord {
|
|
39
|
+
pendingApprovalAt?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read the approval marker off any record coming back from the surface.
|
|
44
|
+
* Structural — works for consumers that only know `OracleTaskRecord`.
|
|
45
|
+
*/
|
|
46
|
+
export function pendingApprovalOf(
|
|
47
|
+
record: OracleTaskRecord,
|
|
48
|
+
): string | undefined {
|
|
49
|
+
if (
|
|
50
|
+
'pendingApprovalAt' in record &&
|
|
51
|
+
typeof record.pendingApprovalAt === 'string'
|
|
52
|
+
) {
|
|
53
|
+
return record.pendingApprovalAt;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** One audit row per fired run / approval decision. */
|
|
59
|
+
export interface TaskRunEntry {
|
|
60
|
+
runId: string;
|
|
61
|
+
taskId: string;
|
|
62
|
+
startedAt: string;
|
|
63
|
+
finishedAt?: string;
|
|
64
|
+
/** true = delivered, false = failed, undefined = no run happened (approval bookkeeping). */
|
|
65
|
+
ok?: boolean;
|
|
66
|
+
detail?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type TaskRow = {
|
|
70
|
+
id: string;
|
|
71
|
+
title: string;
|
|
72
|
+
spec: string;
|
|
73
|
+
schedule_json: string;
|
|
74
|
+
status: string;
|
|
75
|
+
approval: string;
|
|
76
|
+
created_at: string;
|
|
77
|
+
updated_at: string;
|
|
78
|
+
next_run_at: number | bigint | null;
|
|
79
|
+
last_run_at: string | null;
|
|
80
|
+
last_result_json: string | null;
|
|
81
|
+
consecutive_failures: number;
|
|
82
|
+
pending_approval_at: string | null;
|
|
83
|
+
delivery_room_id: string | null;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type RunRow = {
|
|
87
|
+
run_id: string;
|
|
88
|
+
task_id: string;
|
|
89
|
+
started_at: string;
|
|
90
|
+
finished_at: string | null;
|
|
91
|
+
ok: number | null;
|
|
92
|
+
detail: string | null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const TASK_COLUMNS = `id, title, spec, schedule_json, status, approval, created_at,
|
|
96
|
+
updated_at, next_run_at, last_run_at, last_result_json, consecutive_failures, pending_approval_at,
|
|
97
|
+
delivery_room_id`;
|
|
98
|
+
|
|
99
|
+
function isTaskStatus(value: string): value is OracleTaskStatus {
|
|
100
|
+
return (TASK_STATUSES as readonly string[]).includes(value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isApproval(value: string): value is 'never' | 'before-action' {
|
|
104
|
+
return (TASK_APPROVALS as readonly string[]).includes(value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseLastResult(
|
|
108
|
+
json: string,
|
|
109
|
+
): OracleTaskRecord['lastResult'] | undefined {
|
|
110
|
+
try {
|
|
111
|
+
const parsed: unknown = JSON.parse(json);
|
|
112
|
+
if (
|
|
113
|
+
parsed !== null &&
|
|
114
|
+
typeof parsed === 'object' &&
|
|
115
|
+
'ok' in parsed &&
|
|
116
|
+
typeof parsed.ok === 'boolean' &&
|
|
117
|
+
'summary' in parsed &&
|
|
118
|
+
typeof parsed.summary === 'string' &&
|
|
119
|
+
'at' in parsed &&
|
|
120
|
+
typeof parsed.at === 'string'
|
|
121
|
+
) {
|
|
122
|
+
return { ok: parsed.ok, summary: parsed.summary, at: parsed.at };
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
} catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function rowToRecord(row: TaskRow): TaskRecord {
|
|
131
|
+
if (!isTaskStatus(row.status)) {
|
|
132
|
+
throw new Error(`task ${row.id} has unknown status '${row.status}'`);
|
|
133
|
+
}
|
|
134
|
+
if (!isApproval(row.approval)) {
|
|
135
|
+
throw new Error(`task ${row.id} has unknown approval '${row.approval}'`);
|
|
136
|
+
}
|
|
137
|
+
const schedule: OracleTaskSchedule = TaskScheduleSchema.parse(
|
|
138
|
+
JSON.parse(row.schedule_json),
|
|
139
|
+
);
|
|
140
|
+
const record: TaskRecord = {
|
|
141
|
+
id: row.id,
|
|
142
|
+
title: row.title,
|
|
143
|
+
intent: specIntentOf(row.spec),
|
|
144
|
+
schedule,
|
|
145
|
+
status: row.status,
|
|
146
|
+
approval: row.approval,
|
|
147
|
+
createdAt: row.created_at,
|
|
148
|
+
updatedAt: row.updated_at,
|
|
149
|
+
consecutiveFailures: row.consecutive_failures,
|
|
150
|
+
};
|
|
151
|
+
if (row.next_run_at !== null) {
|
|
152
|
+
record.nextRunAt = new Date(Number(row.next_run_at)).toISOString();
|
|
153
|
+
}
|
|
154
|
+
if (row.last_run_at !== null) record.lastRunAt = row.last_run_at;
|
|
155
|
+
if (row.last_result_json !== null) {
|
|
156
|
+
const lastResult = parseLastResult(row.last_result_json);
|
|
157
|
+
if (lastResult) record.lastResult = lastResult;
|
|
158
|
+
}
|
|
159
|
+
if (row.pending_approval_at !== null) {
|
|
160
|
+
record.pendingApprovalAt = row.pending_approval_at;
|
|
161
|
+
}
|
|
162
|
+
if (row.delivery_room_id !== null) {
|
|
163
|
+
record.deliveryRoomId = row.delivery_room_id;
|
|
164
|
+
}
|
|
165
|
+
return record;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function recordParams(record: TaskRecord): SqlParam[] {
|
|
169
|
+
return [
|
|
170
|
+
record.title,
|
|
171
|
+
renderTaskSpec(record),
|
|
172
|
+
JSON.stringify(record.schedule),
|
|
173
|
+
record.status,
|
|
174
|
+
record.approval,
|
|
175
|
+
record.createdAt,
|
|
176
|
+
record.updatedAt,
|
|
177
|
+
record.nextRunAt !== undefined ? Date.parse(record.nextRunAt) : null,
|
|
178
|
+
record.lastRunAt ?? null,
|
|
179
|
+
record.lastResult !== undefined ? JSON.stringify(record.lastResult) : null,
|
|
180
|
+
record.consecutiveFailures,
|
|
181
|
+
record.pendingApprovalAt ?? null,
|
|
182
|
+
record.deliveryRoomId ?? null,
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export class TasksStore {
|
|
187
|
+
private setupPromise: Promise<void> | undefined;
|
|
188
|
+
|
|
189
|
+
constructor(readonly db: DoSqliteDatabase) {}
|
|
190
|
+
|
|
191
|
+
/** Create both tables + indexes. Idempotent, cached like `SessionsStore`. */
|
|
192
|
+
setup(): Promise<void> {
|
|
193
|
+
this.setupPromise ??= this.doSetup().catch((error: unknown) => {
|
|
194
|
+
this.setupPromise = undefined;
|
|
195
|
+
throw error;
|
|
196
|
+
});
|
|
197
|
+
return this.setupPromise;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async doSetup(): Promise<void> {
|
|
201
|
+
await this.db.run(`
|
|
202
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
203
|
+
id TEXT PRIMARY KEY,
|
|
204
|
+
title TEXT NOT NULL,
|
|
205
|
+
spec TEXT NOT NULL,
|
|
206
|
+
schedule_json TEXT NOT NULL,
|
|
207
|
+
status TEXT NOT NULL,
|
|
208
|
+
approval TEXT NOT NULL,
|
|
209
|
+
created_at TEXT NOT NULL,
|
|
210
|
+
updated_at TEXT NOT NULL,
|
|
211
|
+
next_run_at INTEGER,
|
|
212
|
+
last_run_at TEXT,
|
|
213
|
+
last_result_json TEXT,
|
|
214
|
+
consecutive_failures INTEGER NOT NULL DEFAULT 0,
|
|
215
|
+
pending_approval_at TEXT,
|
|
216
|
+
delivery_room_id TEXT
|
|
217
|
+
)`);
|
|
218
|
+
// Files created before dedicated task rooms existed lack the column.
|
|
219
|
+
const columns = await this.db.exec<{ name: string }>(
|
|
220
|
+
`PRAGMA table_info(tasks)`,
|
|
221
|
+
);
|
|
222
|
+
if (!columns.some((c) => c.name === 'delivery_room_id')) {
|
|
223
|
+
await this.db.run(`ALTER TABLE tasks ADD COLUMN delivery_room_id TEXT`);
|
|
224
|
+
}
|
|
225
|
+
await this.db.run(
|
|
226
|
+
`CREATE INDEX IF NOT EXISTS idx_tasks_next_run ON tasks(status, next_run_at)`,
|
|
227
|
+
);
|
|
228
|
+
await this.db.run(`
|
|
229
|
+
CREATE TABLE IF NOT EXISTS task_runs (
|
|
230
|
+
run_id TEXT PRIMARY KEY,
|
|
231
|
+
task_id TEXT NOT NULL,
|
|
232
|
+
started_at TEXT NOT NULL,
|
|
233
|
+
finished_at TEXT,
|
|
234
|
+
ok INTEGER,
|
|
235
|
+
detail TEXT
|
|
236
|
+
)`);
|
|
237
|
+
await this.db.run(
|
|
238
|
+
`CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs(task_id, started_at)`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async insert(record: TaskRecord): Promise<void> {
|
|
243
|
+
await this.setup();
|
|
244
|
+
await this.db.run(
|
|
245
|
+
`INSERT INTO tasks (${TASK_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
246
|
+
[record.id, ...recordParams(record)],
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Full-row rewrite keyed by id. The spec markdown is re-rendered on every save. */
|
|
251
|
+
async save(record: TaskRecord): Promise<void> {
|
|
252
|
+
await this.setup();
|
|
253
|
+
const { changes } = await this.db.run(
|
|
254
|
+
`UPDATE tasks SET title = ?, spec = ?, schedule_json = ?, status = ?, approval = ?,
|
|
255
|
+
created_at = ?, updated_at = ?, next_run_at = ?, last_run_at = ?,
|
|
256
|
+
last_result_json = ?, consecutive_failures = ?, pending_approval_at = ?,
|
|
257
|
+
delivery_room_id = ?
|
|
258
|
+
WHERE id = ?`,
|
|
259
|
+
[...recordParams(record), record.id],
|
|
260
|
+
);
|
|
261
|
+
if (changes === 0) {
|
|
262
|
+
throw new Error(`task ${record.id} not found`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async get(id: string): Promise<TaskRecord | null> {
|
|
267
|
+
await this.setup();
|
|
268
|
+
const row = await this.db.get<TaskRow>(
|
|
269
|
+
`SELECT ${TASK_COLUMNS} FROM tasks WHERE id = ?`,
|
|
270
|
+
[id],
|
|
271
|
+
);
|
|
272
|
+
return row === undefined ? null : rowToRecord(row);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Every task, oldest first (stable listing for tools). */
|
|
276
|
+
async list(): Promise<TaskRecord[]> {
|
|
277
|
+
await this.setup();
|
|
278
|
+
const rows = await this.db.exec<TaskRow>(
|
|
279
|
+
`SELECT ${TASK_COLUMNS} FROM tasks ORDER BY created_at, id`,
|
|
280
|
+
);
|
|
281
|
+
return rows.map(rowToRecord);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Live tasks count toward the per-user cap: active or paused. */
|
|
285
|
+
async countLive(): Promise<number> {
|
|
286
|
+
await this.setup();
|
|
287
|
+
const row = await this.db.get<{ n: number }>(
|
|
288
|
+
`SELECT COUNT(*) AS n FROM tasks WHERE status IN ('active', 'paused')`,
|
|
289
|
+
);
|
|
290
|
+
return row?.n ?? 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Earliest pending deadline (ms epoch) over active tasks, or null. */
|
|
294
|
+
async minNextRunAt(): Promise<number | null> {
|
|
295
|
+
await this.setup();
|
|
296
|
+
const row = await this.db.get<{ next: number | bigint | null }>(
|
|
297
|
+
`SELECT MIN(next_run_at) AS next FROM tasks WHERE status = 'active' AND next_run_at IS NOT NULL`,
|
|
298
|
+
);
|
|
299
|
+
if (row === undefined || row.next === null) return null;
|
|
300
|
+
return Number(row.next);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Active tasks whose next run is due at or before `nowMs`, earliest first. */
|
|
304
|
+
async due(nowMs: number): Promise<TaskRecord[]> {
|
|
305
|
+
await this.setup();
|
|
306
|
+
const rows = await this.db.exec<TaskRow>(
|
|
307
|
+
`SELECT ${TASK_COLUMNS} FROM tasks
|
|
308
|
+
WHERE status = 'active' AND next_run_at IS NOT NULL AND next_run_at <= ?
|
|
309
|
+
ORDER BY next_run_at, id`,
|
|
310
|
+
[nowMs],
|
|
311
|
+
);
|
|
312
|
+
return rows.map(rowToRecord);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async recordRun(entry: TaskRunEntry): Promise<void> {
|
|
316
|
+
await this.setup();
|
|
317
|
+
await this.db.run(
|
|
318
|
+
`INSERT INTO task_runs (run_id, task_id, started_at, finished_at, ok, detail)
|
|
319
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
320
|
+
[
|
|
321
|
+
entry.runId,
|
|
322
|
+
entry.taskId,
|
|
323
|
+
entry.startedAt,
|
|
324
|
+
entry.finishedAt ?? null,
|
|
325
|
+
entry.ok === undefined ? null : entry.ok,
|
|
326
|
+
entry.detail ?? null,
|
|
327
|
+
],
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Audit trail for one task, newest first. */
|
|
332
|
+
async listRuns(taskId: string, limit = 20): Promise<TaskRunEntry[]> {
|
|
333
|
+
await this.setup();
|
|
334
|
+
const rows = await this.db.exec<RunRow>(
|
|
335
|
+
`SELECT run_id, task_id, started_at, finished_at, ok, detail
|
|
336
|
+
FROM task_runs WHERE task_id = ? ORDER BY started_at DESC, run_id DESC LIMIT ?`,
|
|
337
|
+
[taskId, limit],
|
|
338
|
+
);
|
|
339
|
+
return rows.map((row) => {
|
|
340
|
+
const entry: TaskRunEntry = {
|
|
341
|
+
runId: row.run_id,
|
|
342
|
+
taskId: row.task_id,
|
|
343
|
+
startedAt: row.started_at,
|
|
344
|
+
};
|
|
345
|
+
if (row.finished_at !== null) entry.finishedAt = row.finished_at;
|
|
346
|
+
if (row.ok !== null) entry.ok = row.ok !== 0;
|
|
347
|
+
if (row.detail !== null) entry.detail = row.detail;
|
|
348
|
+
return entry;
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|