mercury-agent 0.16.0 → 0.16.2
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 +6 -0
- package/container/Dockerfile +6 -1
- package/container/Dockerfile.base +6 -1
- package/docs/README.md +50 -0
- package/docs/authoring-profiles.md +8 -0
- package/docs/behavior-layers.md +193 -0
- package/docs/extensions.md +2 -0
- package/docs/goals/football-reporter-profile/decisions.md +479 -0
- package/docs/goals/football-reporter-profile/goal.md +91 -0
- package/docs/goals/football-reporter-profile/roadmap.md +196 -0
- package/docs/goals/whatsapp-bot-hardening/README.md +43 -0
- package/docs/goals/whatsapp-bot-hardening/archive/ambient-group-context.md +211 -0
- package/docs/goals/whatsapp-bot-hardening/archive/command-routing-consistency.md +210 -0
- package/docs/goals/whatsapp-bot-hardening/archive/destructive-command-guards.md +218 -0
- package/docs/goals/whatsapp-bot-hardening/archive/docs-extension.md +182 -0
- package/docs/goals/whatsapp-bot-hardening/archive/handoff-2026-08-09.md +588 -0
- package/docs/goals/whatsapp-bot-hardening/archive/media-size-and-silent-drop.md +178 -0
- package/docs/goals/whatsapp-bot-hardening/archive/member-memory.md +199 -0
- package/docs/goals/whatsapp-bot-hardening/archive/message-author-attribution.md +211 -0
- package/docs/goals/whatsapp-bot-hardening/archive/run-from-source-switchover.md +212 -0
- package/docs/goals/whatsapp-bot-hardening/archive/setup-plan-2026-08-06.md +1117 -0
- package/docs/goals/whatsapp-bot-hardening/decisions.md +200 -0
- package/docs/goals/whatsapp-bot-hardening/goal.md +75 -0
- package/docs/goals/whatsapp-bot-hardening/open-threads.md +396 -0
- package/docs/goals/whatsapp-bot-hardening/roadmap.md +245 -0
- package/docs/memory.md +30 -0
- package/docs/profile-guide.md +483 -0
- package/docs/refactor/.gitkeep +0 -0
- package/docs/refactor/archive/.gitkeep +0 -0
- package/docs/refactor/audits/.gitkeep +0 -0
- package/docs/refactor/backlog/.gitkeep +0 -0
- package/docs/skills-guide.md +154 -0
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/feed-watch/config.ts +354 -0
- package/examples/extensions/feed-watch/digest.ts +344 -0
- package/examples/extensions/feed-watch/feeds.ts +341 -0
- package/examples/extensions/feed-watch/index.ts +302 -0
- package/examples/extensions/feed-watch/items.ts +171 -0
- package/examples/extensions/feed-watch/match.ts +107 -0
- package/examples/extensions/feed-watch/prompts/verify.md +35 -0
- package/examples/extensions/feed-watch/skill/SKILL.md +128 -0
- package/examples/extensions/feed-watch/watch.ts +548 -0
- package/examples/extensions/longview/hook.ts +179 -16
- package/examples/extensions/longview/index.ts +13 -2
- package/examples/extensions/longview/prompts/summarize.md +3 -2
- package/examples/extensions/longview/render/telegraph-nodes.ts +21 -1
- package/examples/extensions/longview/summarize.ts +57 -14
- package/examples/extensions/napkin/index.ts +334 -86
- package/examples/extensions/napkin/pi-spawn.ts +196 -0
- package/examples/extensions/pinchtab/index.ts +37 -4
- package/examples/extensions/pinchtab/lib/session-injector.ts +31 -9
- package/examples/extensions/pinchtab/skill/SKILL.md +29 -0
- package/examples/profiles/_template/AGENTS.md +138 -0
- package/examples/profiles/_template/README.md +65 -0
- package/examples/profiles/_template/config.yaml +57 -0
- package/examples/profiles/_template/tasks/daily.md +32 -0
- package/examples/profiles/football-reporter/AGENTS.md +219 -0
- package/examples/profiles/football-reporter/README.md +138 -0
- package/examples/profiles/football-reporter/config.yaml +174 -0
- package/examples/profiles/football-reporter/seed/MEMORY.md +42 -0
- package/examples/profiles/football-reporter/seed/episodes/barcelona-2026-27.md +25 -0
- package/examples/profiles/football-reporter/seed/episodes/beitar-jerusalem-2026-27.md +25 -0
- package/examples/profiles/football-reporter/seed/episodes/maccabi-haifa-2026-27.md +24 -0
- package/examples/profiles/football-reporter/seed/episodes/man-united-2026-27.md +26 -0
- package/examples/profiles/football-reporter/seed/episodes/real-madrid-2026-27.md +25 -0
- package/examples/profiles/football-reporter/seed/napkin-distill.md +55 -0
- package/examples/profiles/football-reporter/tasks/daily-article.md +142 -0
- package/package.json +8 -5
- package/src/adapters/whatsapp.ts +75 -13
- package/src/bridges/whatsapp.ts +6 -4
- package/src/cli/mrctl.ts +9 -1
- package/src/core/handler.ts +51 -6
- package/src/core/routes/dashboard.ts +106 -5
- package/src/core/routes/tasks.ts +28 -0
- package/src/core/runtime.ts +63 -2
- package/src/core/task-scheduler.ts +103 -12
- package/src/extensions/catalog.ts +9 -0
- package/src/profile/space-profile.ts +780 -0
- package/src/storage/db.ts +215 -0
- package/src/types.ts +56 -0
package/src/storage/db.ts
CHANGED
|
@@ -12,6 +12,9 @@ import type {
|
|
|
12
12
|
SpacePreferenceEntry,
|
|
13
13
|
SpaceRole,
|
|
14
14
|
StoredMessage,
|
|
15
|
+
TaskRun,
|
|
16
|
+
TaskRunDetail,
|
|
17
|
+
TaskRunOutcome,
|
|
15
18
|
TaskRunStatus,
|
|
16
19
|
TokenUsage,
|
|
17
20
|
} from "../types.js";
|
|
@@ -23,6 +26,39 @@ import type {
|
|
|
23
26
|
*/
|
|
24
27
|
export const TASK_LAST_ERROR_MAX_LENGTH = 500;
|
|
25
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Ledger rows kept per task. A ceiling rather than a setting: a config key
|
|
31
|
+
* would be a two-surface job (dashboard rows are hardcoded) and nobody has
|
|
32
|
+
* asked to tune it. Questions that reach past this window belong to
|
|
33
|
+
* `token_usage`, which is unbounded.
|
|
34
|
+
*
|
|
35
|
+
* 200 is ~2 months of a thrice-daily task, or a week of a hourly one.
|
|
36
|
+
*/
|
|
37
|
+
export const TASK_RUNS_RETENTION_PER_TASK = 200;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* How long a ledger row outlives a task that no longer exists. Completed
|
|
41
|
+
* one-shots leave their rows behind on purpose (see the task_runs DDL), which
|
|
42
|
+
* is bounded per task but not in the number of tasks — feed-watch style
|
|
43
|
+
* verified one-shots would accumulate forever without a horizon. Thirty days
|
|
44
|
+
* comfortably covers the two-week re-audit the ledger exists to serve.
|
|
45
|
+
*/
|
|
46
|
+
export const TASK_RUNS_ORPHAN_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What a caller must know to write a ledger row: which space it ran in, when
|
|
50
|
+
* it started, whether it was a preview, which attempt it was, and — if the
|
|
51
|
+
* handler reported it — what the run produced.
|
|
52
|
+
*/
|
|
53
|
+
export interface TaskRunLedgerEntry {
|
|
54
|
+
spaceId: string;
|
|
55
|
+
startedAt: number;
|
|
56
|
+
detail?: TaskRunDetail;
|
|
57
|
+
/** True for a "Run now" preview, which shares the code path but no schedule. */
|
|
58
|
+
manual: boolean;
|
|
59
|
+
attempt: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
26
62
|
// `bun:sqlite` caches prepared statements per Database, capped at
|
|
27
63
|
// Database.MAX_QUERY_CACHE_SIZE (default 20). When a 21st distinct statement is
|
|
28
64
|
// prepared the least-recently-used one is evicted WITHOUT being finalized, so
|
|
@@ -173,6 +209,41 @@ export class Db {
|
|
|
173
209
|
CREATE INDEX IF NOT EXISTS idx_tasks_next
|
|
174
210
|
ON tasks(active, next_run_at);
|
|
175
211
|
|
|
212
|
+
-- One row per completed attempt. The tasks table keeps a snapshot of the
|
|
213
|
+
-- last one (last_run_at / last_status / last_error); history needs rows,
|
|
214
|
+
-- not wider columns, and the snapshot is what the dashboard already
|
|
215
|
+
-- reads. Both are written from the same recordTaskRun call, so there is
|
|
216
|
+
-- still exactly one write site per run.
|
|
217
|
+
--
|
|
218
|
+
-- task_id is deliberately NOT a foreign key. A one-shot task is deleted
|
|
219
|
+
-- by the scheduler the moment it completes, and a cascade would erase
|
|
220
|
+
-- its row microseconds after it was written — the normal lifecycle of
|
|
221
|
+
-- an at-task, not an edge case. Rows outlive their task; the user-facing
|
|
222
|
+
-- deletes (deleteTask, deleteSpace) clean up explicitly, and rows left
|
|
223
|
+
-- behind by completed one-shots age out past TASK_RUNS_ORPHAN_MAX_AGE_MS.
|
|
224
|
+
CREATE TABLE IF NOT EXISTS task_runs (
|
|
225
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
226
|
+
task_id INTEGER NOT NULL,
|
|
227
|
+
space_id TEXT NOT NULL,
|
|
228
|
+
started_at INTEGER NOT NULL,
|
|
229
|
+
finished_at INTEGER NOT NULL,
|
|
230
|
+
status TEXT NOT NULL,
|
|
231
|
+
outcome TEXT,
|
|
232
|
+
model TEXT,
|
|
233
|
+
input_tokens INTEGER,
|
|
234
|
+
output_tokens INTEGER,
|
|
235
|
+
total_tokens INTEGER,
|
|
236
|
+
cost REAL,
|
|
237
|
+
reply_chars INTEGER,
|
|
238
|
+
files INTEGER,
|
|
239
|
+
manual INTEGER NOT NULL DEFAULT 0,
|
|
240
|
+
attempt INTEGER NOT NULL DEFAULT 1,
|
|
241
|
+
error TEXT
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
CREATE INDEX IF NOT EXISTS idx_task_runs_task
|
|
245
|
+
ON task_runs(task_id, started_at DESC);
|
|
246
|
+
|
|
176
247
|
CREATE TABLE IF NOT EXISTS chat_state (
|
|
177
248
|
space_id TEXT PRIMARY KEY,
|
|
178
249
|
min_message_id INTEGER NOT NULL DEFAULT 0,
|
|
@@ -578,6 +649,7 @@ export class Db {
|
|
|
578
649
|
const tasks = this.db
|
|
579
650
|
.query("DELETE FROM tasks WHERE space_id = ?")
|
|
580
651
|
.run(spaceId).changes;
|
|
652
|
+
this.db.query("DELETE FROM task_runs WHERE space_id = ?").run(spaceId);
|
|
581
653
|
const chatState = this.db
|
|
582
654
|
.query("DELETE FROM chat_state WHERE space_id = ?")
|
|
583
655
|
.run(spaceId).changes;
|
|
@@ -964,6 +1036,31 @@ export class Db {
|
|
|
964
1036
|
.run(JSON.stringify(meta), now, messageId);
|
|
965
1037
|
}
|
|
966
1038
|
|
|
1039
|
+
/**
|
|
1040
|
+
* Remove one message row. Returns whether a row was deleted.
|
|
1041
|
+
*
|
|
1042
|
+
* Used for a turn that was written and then decided against — a scheduled
|
|
1043
|
+
* run whose reply turns out to be a no-op leaves a prompt in the history
|
|
1044
|
+
* that no answer will ever follow.
|
|
1045
|
+
*
|
|
1046
|
+
* Both tables that can reference a message are cleared first. `foreign_keys`
|
|
1047
|
+
* is ON, so a platform-id row or another message's `reply_to_id` pointing at
|
|
1048
|
+
* this one would otherwise turn the delete into a constraint failure rather
|
|
1049
|
+
* than a no-op. The `reply_to_id` pointers are nulled rather than followed:
|
|
1050
|
+
* severing a chain is right here, cascading it would delete a real answer.
|
|
1051
|
+
*/
|
|
1052
|
+
deleteMessage(id: number): boolean {
|
|
1053
|
+
this.db
|
|
1054
|
+
.query("DELETE FROM message_platform_ids WHERE mercury_message_id = ?")
|
|
1055
|
+
.run(id);
|
|
1056
|
+
this.db
|
|
1057
|
+
.query("UPDATE messages SET reply_to_id = NULL WHERE reply_to_id = ?")
|
|
1058
|
+
.run(id);
|
|
1059
|
+
return (
|
|
1060
|
+
this.db.query("DELETE FROM messages WHERE id = ?").run(id).changes > 0
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
967
1064
|
clearMessages(spaceId: string): void {
|
|
968
1065
|
this.db.query("DELETE FROM messages WHERE space_id = ?").run(spaceId);
|
|
969
1066
|
}
|
|
@@ -1333,6 +1430,7 @@ export class Db {
|
|
|
1333
1430
|
status: TaskRunStatus,
|
|
1334
1431
|
error?: string | null,
|
|
1335
1432
|
at = Date.now(),
|
|
1433
|
+
ledger?: TaskRunLedgerEntry,
|
|
1336
1434
|
): void {
|
|
1337
1435
|
const message =
|
|
1338
1436
|
status === "error" && error
|
|
@@ -1345,6 +1443,115 @@ export class Db {
|
|
|
1345
1443
|
WHERE id = ?`,
|
|
1346
1444
|
)
|
|
1347
1445
|
.run(at, status, message, Date.now(), id);
|
|
1446
|
+
|
|
1447
|
+
// A four-argument call is exactly the behaviour it always had. Only a
|
|
1448
|
+
// caller that can say *what the run produced* writes history.
|
|
1449
|
+
if (!ledger) return;
|
|
1450
|
+
const usage = ledger.detail?.usage;
|
|
1451
|
+
const outcome: TaskRunOutcome | null =
|
|
1452
|
+
status === "error" ? "error" : (ledger.detail?.outcome ?? null);
|
|
1453
|
+
const tx = this.db.transaction(() => {
|
|
1454
|
+
this.db
|
|
1455
|
+
.query(
|
|
1456
|
+
`INSERT INTO task_runs(
|
|
1457
|
+
task_id, space_id, started_at, finished_at, status, outcome,
|
|
1458
|
+
model, input_tokens, output_tokens, total_tokens, cost,
|
|
1459
|
+
reply_chars, files, manual, attempt, error
|
|
1460
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1461
|
+
)
|
|
1462
|
+
.run(
|
|
1463
|
+
id,
|
|
1464
|
+
ledger.spaceId,
|
|
1465
|
+
ledger.startedAt,
|
|
1466
|
+
at,
|
|
1467
|
+
status,
|
|
1468
|
+
outcome,
|
|
1469
|
+
usage?.model ?? null,
|
|
1470
|
+
usage?.inputTokens ?? null,
|
|
1471
|
+
usage?.outputTokens ?? null,
|
|
1472
|
+
usage?.totalTokens ?? null,
|
|
1473
|
+
usage?.cost ?? null,
|
|
1474
|
+
ledger.detail?.replyChars ?? null,
|
|
1475
|
+
ledger.detail?.files ?? null,
|
|
1476
|
+
ledger.manual ? 1 : 0,
|
|
1477
|
+
ledger.attempt,
|
|
1478
|
+
message,
|
|
1479
|
+
);
|
|
1480
|
+
// Prune inside the same transaction: a reader can never see the table
|
|
1481
|
+
// over its ceiling, and a failed insert takes its own prune with it.
|
|
1482
|
+
this.db
|
|
1483
|
+
.query(
|
|
1484
|
+
`DELETE FROM task_runs
|
|
1485
|
+
WHERE task_id = ?
|
|
1486
|
+
AND id NOT IN (
|
|
1487
|
+
SELECT id FROM task_runs WHERE task_id = ? ORDER BY id DESC LIMIT ?
|
|
1488
|
+
)`,
|
|
1489
|
+
)
|
|
1490
|
+
.run(id, id, TASK_RUNS_RETENTION_PER_TASK);
|
|
1491
|
+
// Rows whose task is gone age out. Cheap: tasks is tiny and task_runs is
|
|
1492
|
+
// bounded, and it runs once per scheduled run, not per read.
|
|
1493
|
+
this.db
|
|
1494
|
+
.query(
|
|
1495
|
+
`DELETE FROM task_runs
|
|
1496
|
+
WHERE finished_at < ?
|
|
1497
|
+
AND task_id NOT IN (SELECT id FROM tasks)`,
|
|
1498
|
+
)
|
|
1499
|
+
.run(at - TASK_RUNS_ORPHAN_MAX_AGE_MS);
|
|
1500
|
+
});
|
|
1501
|
+
tx();
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/**
|
|
1505
|
+
* The last `limit` attempts of a task, newest first. Empty for a task that
|
|
1506
|
+
* predates the ledger — its history was never recorded, and inventing rows
|
|
1507
|
+
* for it would be worse than saying so.
|
|
1508
|
+
*/
|
|
1509
|
+
listTaskRuns(taskId: number, limit = 50): TaskRun[] {
|
|
1510
|
+
const capped = Number.isFinite(limit)
|
|
1511
|
+
? Math.max(1, Math.min(TASK_RUNS_RETENTION_PER_TASK, Math.floor(limit)))
|
|
1512
|
+
: 50;
|
|
1513
|
+
return this.db
|
|
1514
|
+
.query(
|
|
1515
|
+
`SELECT id,
|
|
1516
|
+
task_id as taskId,
|
|
1517
|
+
space_id as spaceId,
|
|
1518
|
+
started_at as startedAt,
|
|
1519
|
+
finished_at as finishedAt,
|
|
1520
|
+
status,
|
|
1521
|
+
outcome,
|
|
1522
|
+
model,
|
|
1523
|
+
input_tokens as inputTokens,
|
|
1524
|
+
output_tokens as outputTokens,
|
|
1525
|
+
total_tokens as totalTokens,
|
|
1526
|
+
cost,
|
|
1527
|
+
reply_chars as replyChars,
|
|
1528
|
+
files,
|
|
1529
|
+
manual,
|
|
1530
|
+
attempt,
|
|
1531
|
+
error
|
|
1532
|
+
FROM task_runs
|
|
1533
|
+
WHERE task_id = ?
|
|
1534
|
+
ORDER BY id DESC
|
|
1535
|
+
LIMIT ?`,
|
|
1536
|
+
)
|
|
1537
|
+
.all(taskId, capped) as TaskRun[];
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* Replace a task's prompt in place. Schedule, status and ids are untouched,
|
|
1542
|
+
* so a task whose wording changes keeps its next run and its history.
|
|
1543
|
+
*
|
|
1544
|
+
* The scheduler re-reads the row before each run, so a change lands on the
|
|
1545
|
+
* next run; a run already in flight keeps the prompt it started with.
|
|
1546
|
+
*
|
|
1547
|
+
* Returns whether a row was updated — an unknown id is a caller error, not
|
|
1548
|
+
* an exception, matching `deleteTask`.
|
|
1549
|
+
*/
|
|
1550
|
+
updateTaskPrompt(id: number, prompt: string): boolean {
|
|
1551
|
+
const result = this.db
|
|
1552
|
+
.query("UPDATE tasks SET prompt = ?, updated_at = ? WHERE id = ?")
|
|
1553
|
+
.run(prompt, Date.now(), id);
|
|
1554
|
+
return result.changes > 0;
|
|
1348
1555
|
}
|
|
1349
1556
|
|
|
1350
1557
|
setTaskActive(id: number, active: boolean): void {
|
|
@@ -1353,10 +1560,18 @@ export class Db {
|
|
|
1353
1560
|
.run(active ? 1 : 0, Date.now(), id);
|
|
1354
1561
|
}
|
|
1355
1562
|
|
|
1563
|
+
/**
|
|
1564
|
+
* A user deleting a task takes its run history with it. `deleteTaskById`
|
|
1565
|
+
* deliberately does not: that is the scheduler's path for a completed
|
|
1566
|
+
* one-shot, whose history is the whole point of keeping.
|
|
1567
|
+
*/
|
|
1356
1568
|
deleteTask(id: number, spaceId: string): boolean {
|
|
1357
1569
|
const result = this.db
|
|
1358
1570
|
.query("DELETE FROM tasks WHERE id = ? AND space_id = ?")
|
|
1359
1571
|
.run(id, spaceId);
|
|
1572
|
+
if (result.changes > 0) {
|
|
1573
|
+
this.db.query("DELETE FROM task_runs WHERE task_id = ?").run(id);
|
|
1574
|
+
}
|
|
1360
1575
|
return result.changes > 0;
|
|
1361
1576
|
}
|
|
1362
1577
|
|
package/src/types.ts
CHANGED
|
@@ -106,6 +106,62 @@ export interface ScheduledTask {
|
|
|
106
106
|
*/
|
|
107
107
|
export type TaskRunStatus = "ok" | "error";
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* What a scheduled run *produced*, as recorded in the run ledger.
|
|
111
|
+
*
|
|
112
|
+
* Deliberately wider than `TaskRunStatus`, and deliberately not a
|
|
113
|
+
* replacement for it: `posted` and `no_update` are both `status: "ok"`, and
|
|
114
|
+
* telling them apart is the question the 48-hour audit had to read an entire
|
|
115
|
+
* chat to answer. Keeping the two-valued snapshot on the task row means
|
|
116
|
+
* nothing downstream has to learn a new vocabulary.
|
|
117
|
+
*
|
|
118
|
+
* Delivery is not part of this. Whether the space actually saw the output is
|
|
119
|
+
* governed by the task's `silent` flag; folding the two together would make a
|
|
120
|
+
* silent task indistinguishable from a quiet one.
|
|
121
|
+
*/
|
|
122
|
+
export type TaskRunOutcome = "posted" | "no_update" | "error";
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* What a task handler reports back about a run it completed. Optional at the
|
|
126
|
+
* scheduler boundary: a handler that returns nothing still gets a ledger row,
|
|
127
|
+
* with `outcome` null rather than a guess.
|
|
128
|
+
*/
|
|
129
|
+
export interface TaskRunDetail {
|
|
130
|
+
/** `error` is the scheduler's to write, never the handler's. */
|
|
131
|
+
outcome: Exclude<TaskRunOutcome, "error">;
|
|
132
|
+
/** Model, tokens and cost. Absent on an old agent image or non-JSON pi output. */
|
|
133
|
+
usage?: TokenUsage;
|
|
134
|
+
/** Length of the reply the run produced, before any send-path suppression. */
|
|
135
|
+
replyChars: number;
|
|
136
|
+
/** Files the run produced. A no-op reply with files still counts as posted. */
|
|
137
|
+
files: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** One completed attempt, as stored in `task_runs`. */
|
|
141
|
+
export interface TaskRun {
|
|
142
|
+
id: number;
|
|
143
|
+
taskId: number;
|
|
144
|
+
spaceId: string;
|
|
145
|
+
startedAt: number;
|
|
146
|
+
finishedAt: number;
|
|
147
|
+
status: TaskRunStatus;
|
|
148
|
+
/** Null when the handler reported no detail. */
|
|
149
|
+
outcome: TaskRunOutcome | null;
|
|
150
|
+
/** The model that actually ran, not the leg index — see the spec. */
|
|
151
|
+
model: string | null;
|
|
152
|
+
inputTokens: number | null;
|
|
153
|
+
outputTokens: number | null;
|
|
154
|
+
totalTokens: number | null;
|
|
155
|
+
cost: number | null;
|
|
156
|
+
replyChars: number | null;
|
|
157
|
+
files: number | null;
|
|
158
|
+
/** 1 when the run came from a "Run now" preview rather than the schedule. */
|
|
159
|
+
manual: number;
|
|
160
|
+
/** Attempt number within the current due cycle, starting at 1. */
|
|
161
|
+
attempt: number;
|
|
162
|
+
error: string | null;
|
|
163
|
+
}
|
|
164
|
+
|
|
109
165
|
export interface Space {
|
|
110
166
|
/** User-chosen slug, primary key. e.g. "family", "work" */
|
|
111
167
|
id: string;
|