@jungjaehoon/mama-os 0.48.0 → 0.48.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/dist/agent/agent-loop.d.ts.map +1 -1
- package/dist/agent/agent-loop.js +4 -0
- package/dist/agent/agent-loop.js.map +1 -1
- package/dist/agent/code-act/host-bridge.d.ts.map +1 -1
- package/dist/agent/code-act/host-bridge.js +47 -5
- package/dist/agent/code-act/host-bridge.js.map +1 -1
- package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
- package/dist/agent/gateway-tool-executor.js +110 -0
- package/dist/agent/gateway-tool-executor.js.map +1 -1
- package/dist/agent/gateway-tools.md +4 -3
- package/dist/agent/tool-registry.d.ts.map +1 -1
- package/dist/agent/tool-registry.js +33 -6
- package/dist/agent/tool-registry.js.map +1 -1
- package/dist/agent/types.d.ts +10 -1
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +27 -3
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/config/types.d.ts.map +1 -1
- package/dist/cli/config/types.js +1 -0
- package/dist/cli/config/types.js.map +1 -1
- package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
- package/dist/cli/runtime/api-routes-init.js +142 -15
- package/dist/cli/runtime/api-routes-init.js.map +1 -1
- package/dist/db/migrations/operator-task-classification.d.ts +15 -0
- package/dist/db/migrations/operator-task-classification.d.ts.map +1 -0
- package/dist/db/migrations/operator-task-classification.js +25 -0
- package/dist/db/migrations/operator-task-classification.js.map +1 -0
- package/dist/evidence/effects.d.ts +2 -1
- package/dist/evidence/effects.d.ts.map +1 -1
- package/dist/evidence/effects.js.map +1 -1
- package/dist/gateways/message-router.d.ts.map +1 -1
- package/dist/gateways/message-router.js +2 -1
- package/dist/gateways/message-router.js.map +1 -1
- package/dist/multi-agent/dashboard-agent-persona.d.ts.map +1 -1
- package/dist/multi-agent/dashboard-agent-persona.js +7 -8
- package/dist/multi-agent/dashboard-agent-persona.js.map +1 -1
- package/dist/multi-agent/wiki-agent-persona.d.ts +7 -6
- package/dist/multi-agent/wiki-agent-persona.d.ts.map +1 -1
- package/dist/multi-agent/wiki-agent-persona.js +9 -93
- package/dist/multi-agent/wiki-agent-persona.js.map +1 -1
- package/dist/operator/action-verifier.d.ts +1 -1
- package/dist/operator/action-verifier.d.ts.map +1 -1
- package/dist/operator/action-verifier.js +4 -0
- package/dist/operator/action-verifier.js.map +1 -1
- package/dist/operator/board-reconcile.d.ts +1 -1
- package/dist/operator/board-reconcile.js +1 -1
- package/dist/operator/owner-event-outcome.d.ts +6 -0
- package/dist/operator/owner-event-outcome.d.ts.map +1 -1
- package/dist/operator/owner-event-outcome.js +6 -3
- package/dist/operator/owner-event-outcome.js.map +1 -1
- package/dist/operator/owner-event-policy.d.ts.map +1 -1
- package/dist/operator/owner-event-policy.js +9 -4
- package/dist/operator/owner-event-policy.js.map +1 -1
- package/dist/operator/owner-event-prompt.d.ts.map +1 -1
- package/dist/operator/owner-event-prompt.js +4 -0
- package/dist/operator/owner-event-prompt.js.map +1 -1
- package/dist/operator/task-ledger.d.ts +86 -1
- package/dist/operator/task-ledger.d.ts.map +1 -1
- package/dist/operator/task-ledger.js +285 -8
- package/dist/operator/task-ledger.js.map +1 -1
- package/dist/operator/task-list-views.d.ts.map +1 -1
- package/dist/operator/task-list-views.js +28 -0
- package/dist/operator/task-list-views.js.map +1 -1
- package/dist/operator/temporal-reconcile.d.ts +1 -0
- package/dist/operator/temporal-reconcile.d.ts.map +1 -1
- package/dist/operator/temporal-reconcile.js +1 -0
- package/dist/operator/temporal-reconcile.js.map +1 -1
- package/dist/operator/wiki-continuity.d.ts +151 -0
- package/dist/operator/wiki-continuity.d.ts.map +1 -0
- package/dist/operator/wiki-continuity.js +215 -0
- package/dist/operator/wiki-continuity.js.map +1 -0
- package/dist/operator/worker-run.js +1 -1
- package/dist/operator/worker-run.js.map +1 -1
- package/dist/operator/workorder-consumer.d.ts.map +1 -1
- package/dist/operator/workorder-consumer.js +10 -7
- package/dist/operator/workorder-consumer.js.map +1 -1
- package/dist/operator/workorder-hooks.d.ts +1 -1
- package/dist/operator/workorder-publishers.d.ts +26 -4
- package/dist/operator/workorder-publishers.d.ts.map +1 -1
- package/dist/operator/workorder-publishers.js +136 -1
- package/dist/operator/workorder-publishers.js.map +1 -1
- package/dist/wiki/obsidian-writer.d.ts.map +1 -1
- package/dist/wiki/obsidian-writer.js +14 -2
- package/dist/wiki/obsidian-writer.js.map +1 -1
- package/dist/wiki/wiki-turn-contract.d.ts +19 -0
- package/dist/wiki/wiki-turn-contract.d.ts.map +1 -0
- package/dist/wiki/wiki-turn-contract.js +58 -0
- package/dist/wiki/wiki-turn-contract.js.map +1 -0
- package/package.json +1 -1
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* workorder state. Implements the pre-existing `TaskSource` interface
|
|
8
8
|
* (operator-interfaces.ts) so the board projects one task model, not two.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* Owner conversations create rows through task_create. Reconcile runs update or
|
|
11
|
+
* reclassify existing rows through the task_update/task_reclassify gateway
|
|
11
12
|
* tools; the pipeline board slot is a projection of `list({order:
|
|
12
13
|
* 'deadline_priority'})`. The agent proposes task changes, while this ledger
|
|
13
14
|
* enforces revisions, temporal ownership, idempotency, and atomic receipts.
|
|
@@ -20,11 +21,12 @@
|
|
|
20
21
|
* (start.ts opens and closes it once) - deliberately no close() here.
|
|
21
22
|
*/
|
|
22
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
-
exports.TaskLedger = exports.WORKORDER_CHANNEL_PREFIX = exports.TEMPORAL_WORKORDER_MAX_ATTEMPTS = exports.WORKORDER_KINDS = exports.TASK_KINDS = exports.TASK_PRIORITIES = exports.TASK_STATUSES = void 0;
|
|
24
|
+
exports.TaskLedger = exports.TASK_COMPLETION_CRITERIA_MAX_LENGTH = exports.TASK_RECLASSIFY_REASON_MAX_LENGTH = exports.TASK_RECLASSIFY_DISPOSITIONS = exports.TASK_RESOLUTION_KINDS = exports.WORKORDER_CHANNEL_PREFIX = exports.TEMPORAL_WORKORDER_MAX_ATTEMPTS = exports.WORKORDER_KINDS = exports.TASK_KINDS = exports.TASK_PRIORITIES = exports.TASK_STATUSES = void 0;
|
|
24
25
|
const node_crypto_1 = require("node:crypto");
|
|
25
26
|
const operator_task_temporal_js_1 = require("../db/migrations/operator-task-temporal.js");
|
|
26
27
|
const operator_task_external_lifecycle_js_1 = require("../db/migrations/operator-task-external-lifecycle.js");
|
|
27
28
|
const operator_task_review_lifecycle_js_1 = require("../db/migrations/operator-task-review-lifecycle.js");
|
|
29
|
+
const operator_task_classification_js_1 = require("../db/migrations/operator-task-classification.js");
|
|
28
30
|
const effects_js_1 = require("../evidence/effects.js");
|
|
29
31
|
const temporal_effect_js_1 = require("./temporal-effect.js");
|
|
30
32
|
const task_temporal_js_1 = require("./task-temporal.js");
|
|
@@ -73,6 +75,28 @@ exports.WORKORDER_CHANNEL_PREFIX = 'workorder:';
|
|
|
73
75
|
const EFFECT_CAUSE_CHUNK = 500;
|
|
74
76
|
const KNOWN_WORKORDER_CHANNELS = exports.WORKORDER_KINDS.map((workKind) => `${exports.WORKORDER_CHANNEL_PREFIX}${workKind}`);
|
|
75
77
|
const KNOWN_WORKORDER_CHANNELS_SQL = KNOWN_WORKORDER_CHANNELS.map(() => '?').join(',');
|
|
78
|
+
/**
|
|
79
|
+
* Why a row reached its terminal state. Without this the agent cannot tell
|
|
80
|
+
* completed work from an item that was never a task, which is how connector
|
|
81
|
+
* observations, principles and open questions accumulated as owner rows.
|
|
82
|
+
*/
|
|
83
|
+
exports.TASK_RESOLUTION_KINDS = [
|
|
84
|
+
'completed_evidence',
|
|
85
|
+
'completed_no_issue',
|
|
86
|
+
'non_task_record',
|
|
87
|
+
'non_task_memory',
|
|
88
|
+
];
|
|
89
|
+
/** The recorrection an agent may apply to an existing owner row. */
|
|
90
|
+
exports.TASK_RECLASSIFY_DISPOSITIONS = [
|
|
91
|
+
'completed_evidence',
|
|
92
|
+
'completed_no_issue',
|
|
93
|
+
'non_task_record',
|
|
94
|
+
'non_task_memory',
|
|
95
|
+
'reopen',
|
|
96
|
+
];
|
|
97
|
+
/** Upper bound on the preserved reason, matching latest_event's practical size. */
|
|
98
|
+
exports.TASK_RECLASSIFY_REASON_MAX_LENGTH = 2000;
|
|
99
|
+
exports.TASK_COMPLETION_CRITERIA_MAX_LENGTH = 500;
|
|
76
100
|
const PRIORITY_RANK = { high: 0, normal: 1, low: 2 };
|
|
77
101
|
function encodeListCursor(payload) {
|
|
78
102
|
return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
|
|
@@ -122,6 +146,18 @@ function assertIsoDate(value, field) {
|
|
|
122
146
|
throw new Error(`${field} must be an ISO date (YYYY-MM-DD), got: ${value}`);
|
|
123
147
|
}
|
|
124
148
|
}
|
|
149
|
+
function normalizeCompletionCriteria(value, operation) {
|
|
150
|
+
if (value === undefined || value === null)
|
|
151
|
+
return null;
|
|
152
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
153
|
+
throw new Error(`${operation}: completion_criteria must be a non-empty string`);
|
|
154
|
+
}
|
|
155
|
+
const normalized = value.trim();
|
|
156
|
+
if (normalized.length > exports.TASK_COMPLETION_CRITERIA_MAX_LENGTH) {
|
|
157
|
+
throw new Error(`${operation}: completion_criteria must be at most ${exports.TASK_COMPLETION_CRITERIA_MAX_LENGTH} characters`);
|
|
158
|
+
}
|
|
159
|
+
return normalized;
|
|
160
|
+
}
|
|
125
161
|
function rowToRecord(row, now, timeZone) {
|
|
126
162
|
return {
|
|
127
163
|
id: row.id,
|
|
@@ -153,6 +189,8 @@ function rowToRecord(row, now, timeZone) {
|
|
|
153
189
|
}, now, timeZone),
|
|
154
190
|
reviewStartedAt: row.review_started_at,
|
|
155
191
|
reviewAnchorEventId: row.review_anchor_event_id,
|
|
192
|
+
completionCriteria: row.completion_criteria ?? null,
|
|
193
|
+
resolutionKind: (row.resolution_kind ?? null),
|
|
156
194
|
createdAt: row.created_at,
|
|
157
195
|
updatedAt: row.updated_at,
|
|
158
196
|
};
|
|
@@ -226,6 +264,8 @@ class TaskLedger {
|
|
|
226
264
|
last_temporal_attempt_id INTEGER,
|
|
227
265
|
review_started_at INTEGER,
|
|
228
266
|
review_anchor_event_id TEXT,
|
|
267
|
+
completion_criteria TEXT,
|
|
268
|
+
resolution_kind TEXT,
|
|
229
269
|
created_at INTEGER NOT NULL,
|
|
230
270
|
updated_at INTEGER NOT NULL`;
|
|
231
271
|
// Unique-index predicate excludes terminal rows so a terminal keyed workorder
|
|
@@ -292,6 +332,7 @@ class TaskLedger {
|
|
|
292
332
|
(0, operator_task_temporal_js_1.applyOperatorTaskTemporalMigration)(this.db);
|
|
293
333
|
(0, operator_task_external_lifecycle_js_1.applyOperatorTaskExternalLifecycleMigration)(this.db);
|
|
294
334
|
(0, operator_task_review_lifecycle_js_1.applyOperatorTaskReviewLifecycleMigration)(this.db);
|
|
335
|
+
(0, operator_task_classification_js_1.applyOperatorTaskClassificationMigration)(this.db);
|
|
295
336
|
// Old-predicate unique index (no terminal exclusion) -> swap in place.
|
|
296
337
|
const idxRow = this.db
|
|
297
338
|
.prepare(`SELECT sql FROM sqlite_master WHERE type='index' AND name='idx_operator_tasks_source'`)
|
|
@@ -377,6 +418,16 @@ class TaskLedger {
|
|
|
377
418
|
where.push('updated_at >= ?');
|
|
378
419
|
params.push(filter.updatedSinceMs);
|
|
379
420
|
}
|
|
421
|
+
if (filter.updatedBeforeMs !== undefined) {
|
|
422
|
+
where.push('updated_at < ?');
|
|
423
|
+
params.push(filter.updatedBeforeMs);
|
|
424
|
+
}
|
|
425
|
+
if (filter.qualification === 'qualified') {
|
|
426
|
+
where.push('completion_criteria IS NOT NULL');
|
|
427
|
+
}
|
|
428
|
+
else if (filter.qualification === 'legacy_unqualified') {
|
|
429
|
+
where.push('completion_criteria IS NULL');
|
|
430
|
+
}
|
|
380
431
|
return { where, params };
|
|
381
432
|
}
|
|
382
433
|
/** ORDER BY for both list() and listPage(); the trailing id makes it a TOTAL order,
|
|
@@ -873,7 +924,15 @@ class TaskLedger {
|
|
|
873
924
|
outcome = 'superseded';
|
|
874
925
|
}
|
|
875
926
|
else if (input.decision === 'apply') {
|
|
876
|
-
const
|
|
927
|
+
const terminalEvidence = candidate.proposedStatus === 'done' || candidate.proposedStatus === 'cancelled';
|
|
928
|
+
const transitioned = this.transitionTaskInTransaction(candidate.taskId, {
|
|
929
|
+
status: candidate.proposedStatus,
|
|
930
|
+
latest_event: candidate.evidenceSummary,
|
|
931
|
+
// This candidate is a host-issued current authoritative lifecycle
|
|
932
|
+
// observation. Preserve its semantic completion, or clear an earlier
|
|
933
|
+
// terminal classification when later feedback reopens the same row.
|
|
934
|
+
resolution_kind: terminalEvidence ? 'completed_evidence' : null,
|
|
935
|
+
}, origin, true);
|
|
877
936
|
taskRevisionAfter = transitioned.revision;
|
|
878
937
|
outcome = 'applied';
|
|
879
938
|
}
|
|
@@ -1053,6 +1112,9 @@ class TaskLedger {
|
|
|
1053
1112
|
}
|
|
1054
1113
|
const normalizedDeadline = exactDue?.deadline ?? input.deadline ?? null;
|
|
1055
1114
|
const initialTemporalEpoch = normalizedDeadline !== null ? 1 : 0;
|
|
1115
|
+
// Optional HERE on purpose: the PUBLIC task_create boundary requires it, while
|
|
1116
|
+
// internal/system seeding and legacy callers stay backward compatible.
|
|
1117
|
+
const normalizedCompletionCriteria = normalizeCompletionCriteria(input.completion_criteria, 'task_create');
|
|
1056
1118
|
const now = this.now();
|
|
1057
1119
|
if (input.source_channel && input.source_event_id) {
|
|
1058
1120
|
// kind='owner' probe: an agent-supplied (channel, event) pair can never
|
|
@@ -1073,6 +1135,9 @@ class TaskLedger {
|
|
|
1073
1135
|
...(input.due_at !== undefined ? { due_at: input.due_at } : {}),
|
|
1074
1136
|
...(input.confirmed !== undefined ? { confirmed: input.confirmed } : {}),
|
|
1075
1137
|
...(input.latest_event !== undefined ? { latest_event: input.latest_event } : {}),
|
|
1138
|
+
...(normalizedCompletionCriteria !== null
|
|
1139
|
+
? { completion_criteria: normalizedCompletionCriteria }
|
|
1140
|
+
: {}),
|
|
1076
1141
|
...(input.expected_revision !== undefined
|
|
1077
1142
|
? { expected_revision: input.expected_revision }
|
|
1078
1143
|
: {}),
|
|
@@ -1084,6 +1149,7 @@ class TaskLedger {
|
|
|
1084
1149
|
// carried nothing.
|
|
1085
1150
|
{
|
|
1086
1151
|
...origin,
|
|
1152
|
+
taskCreateUpsert: true,
|
|
1087
1153
|
causeEventIds: origin.causeEventIds?.length
|
|
1088
1154
|
? origin.causeEventIds
|
|
1089
1155
|
: input.source_event_id
|
|
@@ -1099,9 +1165,9 @@ class TaskLedger {
|
|
|
1099
1165
|
.prepare(`INSERT INTO operator_tasks
|
|
1100
1166
|
(title, status, priority, assignee, deadline, due_at, deadline_offset_minutes,
|
|
1101
1167
|
revision, temporal_epoch, source_channel, source_event_id, latest_event,
|
|
1102
|
-
auto_created, confirmed, created_at, updated_at)
|
|
1103
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1104
|
-
.run(input.title.trim(), input.status ?? 'pending', input.priority ?? 'normal', input.assignee ?? null, normalizedDeadline, exactDue?.dueAt ?? null, exactDue?.offsetMinutes ?? null, initialTemporalEpoch, input.source_channel ?? null, input.source_event_id ?? null, input.latest_event ?? null, 1, input.confirmed ? 1 : 0, now, now);
|
|
1168
|
+
auto_created, confirmed, completion_criteria, created_at, updated_at)
|
|
1169
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1170
|
+
.run(input.title.trim(), input.status ?? 'pending', input.priority ?? 'normal', input.assignee ?? null, normalizedDeadline, exactDue?.dueAt ?? null, exactDue?.offsetMinutes ?? null, initialTemporalEpoch, input.source_channel ?? null, input.source_event_id ?? null, input.latest_event ?? null, 1, input.confirmed ? 1 : 0, normalizedCompletionCriteria, now, now);
|
|
1105
1171
|
createdId = Number(result.lastInsertRowid);
|
|
1106
1172
|
this.recordTaskChange({
|
|
1107
1173
|
kind: 'task_create',
|
|
@@ -1142,6 +1208,7 @@ class TaskLedger {
|
|
|
1142
1208
|
source_event_id: input.source_event_id ?? null,
|
|
1143
1209
|
latest_event: input.latest_event ?? null,
|
|
1144
1210
|
confirmed: input.confirmed ? 1 : 0,
|
|
1211
|
+
completion_criteria: normalizedCompletionCriteria,
|
|
1145
1212
|
},
|
|
1146
1213
|
atMs: now,
|
|
1147
1214
|
});
|
|
@@ -1203,6 +1270,148 @@ class TaskLedger {
|
|
|
1203
1270
|
throw error;
|
|
1204
1271
|
}
|
|
1205
1272
|
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Recorrect an existing owner row with a NAMED disposition.
|
|
1275
|
+
*
|
|
1276
|
+
* Records and tasks are separate. Without a semantic terminal reason the agent
|
|
1277
|
+
* could not distinguish completed work from an item that was never a task, so
|
|
1278
|
+
* connector observations, principles, lessons and open questions accumulated as
|
|
1279
|
+
* owner rows and could only be "closed" as if they had been done.
|
|
1280
|
+
*
|
|
1281
|
+
* Every path requires the exact current revision, a non-empty bounded reason
|
|
1282
|
+
* preserved as `latest_event`, an OWNER row, and the same candidate/workorder
|
|
1283
|
+
* authority checks as task_update (TG-03/TG-04/TG-06) - a terminal or foreign
|
|
1284
|
+
* board attempt cannot mutate an unrelated row. The whole transition, its
|
|
1285
|
+
* `resolution_kind` and its effect receipt commit as ONE transaction.
|
|
1286
|
+
*/
|
|
1287
|
+
reclassify(id, input, origin = {}) {
|
|
1288
|
+
const disposition = input?.disposition;
|
|
1289
|
+
if (!exports.TASK_RECLASSIFY_DISPOSITIONS.includes(disposition)) {
|
|
1290
|
+
throw new Error(`task_reclassify: disposition must be one of ${exports.TASK_RECLASSIFY_DISPOSITIONS.join('|')}, got: ${String(disposition)}`);
|
|
1291
|
+
}
|
|
1292
|
+
const reason = typeof input.reason === 'string' ? input.reason.trim() : '';
|
|
1293
|
+
if (reason.length === 0) {
|
|
1294
|
+
throw new Error('task_reclassify: reason must be a non-empty string');
|
|
1295
|
+
}
|
|
1296
|
+
if (reason.length > exports.TASK_RECLASSIFY_REASON_MAX_LENGTH) {
|
|
1297
|
+
throw new Error(`task_reclassify: reason must be at most ${exports.TASK_RECLASSIFY_REASON_MAX_LENGTH} characters`);
|
|
1298
|
+
}
|
|
1299
|
+
if (!Number.isSafeInteger(input.expected_revision) || input.expected_revision < 0) {
|
|
1300
|
+
throw new Error('task_reclassify: expected_revision must be a non-negative integer');
|
|
1301
|
+
}
|
|
1302
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
1303
|
+
try {
|
|
1304
|
+
const existing = this.db.prepare('SELECT * FROM operator_tasks WHERE id = ?').get(id);
|
|
1305
|
+
if (!existing)
|
|
1306
|
+
throw new Error(`task_reclassify: no task with id ${id}`);
|
|
1307
|
+
if (existing.kind === 'system') {
|
|
1308
|
+
throw new Error(`task_reclassify: task ${id} is a system workorder row (owner rows only)`);
|
|
1309
|
+
}
|
|
1310
|
+
if (input.expected_revision !== existing.revision) {
|
|
1311
|
+
throw new Error(`task_reclassify: expected revision ${input.expected_revision}, current ${existing.revision}`);
|
|
1312
|
+
}
|
|
1313
|
+
this.assertTaskReclassificationAuthorized(existing, origin);
|
|
1314
|
+
const patch = this.buildReclassifyPatch(existing, disposition, reason);
|
|
1315
|
+
// Same primitive as task_update: candidate guard, board-attempt liveness,
|
|
1316
|
+
// revision-checked UPDATE, effect receipt, temporal generation supersession.
|
|
1317
|
+
const record = this.transitionTaskInTransaction(id, patch, origin);
|
|
1318
|
+
this.db.exec('COMMIT');
|
|
1319
|
+
return record;
|
|
1320
|
+
}
|
|
1321
|
+
catch (error) {
|
|
1322
|
+
this.db.exec('ROLLBACK');
|
|
1323
|
+
throw error;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
assertTaskReclassificationAuthorized(existing, origin) {
|
|
1327
|
+
if (origin.workOrderAttemptId !== undefined) {
|
|
1328
|
+
const attempt = this.getWorkOrderById(origin.workOrderAttemptId);
|
|
1329
|
+
if (!attempt || attempt.workKind !== 'board' || attempt.status !== 'in_progress') {
|
|
1330
|
+
throw new Error(`task_reclassify: Board workorder ${String(origin.workOrderAttemptId)} is no longer active`);
|
|
1331
|
+
}
|
|
1332
|
+
const { attempts: _attempts, ...payload } = attempt.payload;
|
|
1333
|
+
(0, workorder_publishers_js_1.validateWorkOrderPayload)('board', payload);
|
|
1334
|
+
const candidates = Array.isArray(payload.reclassificationCandidates)
|
|
1335
|
+
? payload.reclassificationCandidates
|
|
1336
|
+
: [];
|
|
1337
|
+
const candidate = candidates.find((item) => item.taskId === existing.id);
|
|
1338
|
+
if (!candidate || candidate.taskRevision !== existing.revision) {
|
|
1339
|
+
throw new Error(`task_reclassify: task ${existing.id} is outside this host-issued Board candidate set`);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
if (origin.reclassificationCauseBound) {
|
|
1343
|
+
const eventIds = (origin.causeEventIds ?? []).filter(effects_js_1.isUsableCause);
|
|
1344
|
+
if (eventIds.length === 0) {
|
|
1345
|
+
throw new Error('task_reclassify: owner-event reclassification requires causal events');
|
|
1346
|
+
}
|
|
1347
|
+
if (existing.source_event_id && eventIds.includes(existing.source_event_id))
|
|
1348
|
+
return;
|
|
1349
|
+
const placeholders = eventIds.map(() => '?').join(',');
|
|
1350
|
+
const channels = this.db
|
|
1351
|
+
.prepare(`SELECT DISTINCT inbox.channel_key
|
|
1352
|
+
FROM owner_event_inbox AS inbox, json_each(inbox.event_ids_json) AS event
|
|
1353
|
+
WHERE event.value IN (${placeholders})`)
|
|
1354
|
+
.all(...eventIds);
|
|
1355
|
+
if (existing.source_channel === null ||
|
|
1356
|
+
!channels.some((row) => row.channel_key === existing.source_channel)) {
|
|
1357
|
+
throw new Error(`task_reclassify: task ${existing.id} is outside this owner-event source channel`);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
buildReclassifyPatch(existing, disposition, reason) {
|
|
1362
|
+
const base = {
|
|
1363
|
+
latest_event: reason,
|
|
1364
|
+
expected_revision: existing.revision,
|
|
1365
|
+
};
|
|
1366
|
+
if (disposition === 'reopen') {
|
|
1367
|
+
if (existing.status !== 'done' && existing.status !== 'cancelled') {
|
|
1368
|
+
throw new Error(`task_reclassify: reopen requires a terminal owner row, task is '${existing.status}'`);
|
|
1369
|
+
}
|
|
1370
|
+
// A reopened row must not inherit a deadline that already passed: it would
|
|
1371
|
+
// read as overdue the moment it reopens. A still-future deadline is kept.
|
|
1372
|
+
const stale = this.isDeadlineInThePast(existing);
|
|
1373
|
+
return {
|
|
1374
|
+
...base,
|
|
1375
|
+
status: 'pending',
|
|
1376
|
+
resolution_kind: null,
|
|
1377
|
+
...(stale ? { deadline: null, due_at: null } : {}),
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
if (existing.status === 'done' || existing.status === 'cancelled') {
|
|
1381
|
+
throw new Error(`task_reclassify: ${disposition} requires an active owner row, task is '${existing.status}'`);
|
|
1382
|
+
}
|
|
1383
|
+
if (disposition === 'completed_evidence') {
|
|
1384
|
+
return { ...base, status: 'done', resolution_kind: 'completed_evidence' };
|
|
1385
|
+
}
|
|
1386
|
+
if (disposition === 'completed_no_issue') {
|
|
1387
|
+
// Owner policy: a PAST deadline plus a complete relevant-source check with no
|
|
1388
|
+
// issue is enough. Without a passed deadline there is nothing to conclude from.
|
|
1389
|
+
if (!this.isDeadlineInThePast(existing)) {
|
|
1390
|
+
throw new Error('task_reclassify: completed_no_issue requires a deadline or due_at already in the past');
|
|
1391
|
+
}
|
|
1392
|
+
return { ...base, status: 'done', resolution_kind: 'completed_no_issue' };
|
|
1393
|
+
}
|
|
1394
|
+
// non_task_record / non_task_memory: this was never a task. Cancelled, not done,
|
|
1395
|
+
// so accounting never reads it as finished work.
|
|
1396
|
+
return {
|
|
1397
|
+
...base,
|
|
1398
|
+
status: 'cancelled',
|
|
1399
|
+
completion_criteria: null,
|
|
1400
|
+
resolution_kind: disposition,
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
/** True when the row carries a deadline/due_at that has already passed. */
|
|
1404
|
+
isDeadlineInThePast(existing) {
|
|
1405
|
+
const state = (0, task_temporal_js_1.deriveTemporalState)({
|
|
1406
|
+
// Terminal rows derive 'closed', which would hide a genuinely past deadline;
|
|
1407
|
+
// ask the temporal question about the CLOCK fields only.
|
|
1408
|
+
status: 'pending',
|
|
1409
|
+
dueAt: existing.due_at,
|
|
1410
|
+
deadlineIso: existing.deadline,
|
|
1411
|
+
deadlineOffsetMinutes: existing.deadline_offset_minutes,
|
|
1412
|
+
}, this.now(), this.timeZone);
|
|
1413
|
+
return state === 'exact_overdue' || state === 'date_overdue';
|
|
1414
|
+
}
|
|
1206
1415
|
/**
|
|
1207
1416
|
* The one mutation primitive for owner-task state. Callers own the outer
|
|
1208
1417
|
* transaction so lifecycle receipts, effects, generation ownership, and the
|
|
@@ -1224,7 +1433,8 @@ class TaskLedger {
|
|
|
1224
1433
|
: null;
|
|
1225
1434
|
const hasLifecyclePatch = Object.prototype.hasOwnProperty.call(patch, 'status') ||
|
|
1226
1435
|
Object.prototype.hasOwnProperty.call(patch, 'due_at') ||
|
|
1227
|
-
Object.prototype.hasOwnProperty.call(patch, 'latest_event')
|
|
1436
|
+
Object.prototype.hasOwnProperty.call(patch, 'latest_event') ||
|
|
1437
|
+
Object.prototype.hasOwnProperty.call(patch, 'completion_criteria');
|
|
1228
1438
|
const isBoardWorkorderLifecycleJudgment = revisionAttempt?.workKind === 'board' && hasLifecyclePatch;
|
|
1229
1439
|
if (isBoardWorkorderLifecycleJudgment && revisionAttempt.status !== 'in_progress') {
|
|
1230
1440
|
throw new Error(`task_update: board workorder ${revisionAttempt.id} is no longer active (${revisionAttempt.status})`);
|
|
@@ -1276,6 +1486,8 @@ class TaskLedger {
|
|
|
1276
1486
|
last_temporal_attempt_id: existing.last_temporal_attempt_id,
|
|
1277
1487
|
review_started_at: existing.review_started_at,
|
|
1278
1488
|
review_anchor_event_id: existing.review_anchor_event_id,
|
|
1489
|
+
completion_criteria: existing.completion_criteria,
|
|
1490
|
+
resolution_kind: existing.resolution_kind,
|
|
1279
1491
|
};
|
|
1280
1492
|
if (patch.title !== undefined)
|
|
1281
1493
|
next.title = patch.title.trim();
|
|
@@ -1289,6 +1501,12 @@ class TaskLedger {
|
|
|
1289
1501
|
next.latest_event = patch.latest_event;
|
|
1290
1502
|
if (patch.confirmed !== undefined)
|
|
1291
1503
|
next.confirmed = patch.confirmed ? 1 : 0;
|
|
1504
|
+
if (patch.completion_criteria !== undefined) {
|
|
1505
|
+
next.completion_criteria = normalizeCompletionCriteria(patch.completion_criteria, 'task_update');
|
|
1506
|
+
}
|
|
1507
|
+
// Host-internal (reclassify only); the public task_update surface rejects it.
|
|
1508
|
+
if (patch.resolution_kind !== undefined)
|
|
1509
|
+
next.resolution_kind = patch.resolution_kind;
|
|
1292
1510
|
const leavingVerifiedReview = existing.status === 'review' &&
|
|
1293
1511
|
patch.status !== undefined &&
|
|
1294
1512
|
patch.status !== 'review' &&
|
|
@@ -1353,6 +1571,16 @@ class TaskLedger {
|
|
|
1353
1571
|
const terminalToOpen = (existing.status === 'done' || existing.status === 'cancelled') &&
|
|
1354
1572
|
next.status !== 'done' &&
|
|
1355
1573
|
next.status !== 'cancelled';
|
|
1574
|
+
if (terminalToOpen && existing.resolution_kind !== null && patch.resolution_kind !== null) {
|
|
1575
|
+
throw new Error('task_update: a classified terminal row must reopen through task_reclassify');
|
|
1576
|
+
}
|
|
1577
|
+
if (patch.status !== undefined &&
|
|
1578
|
+
next.status !== existing.status &&
|
|
1579
|
+
patch.resolution_kind === undefined) {
|
|
1580
|
+
// A generic status change has no semantic reclassification authority. Clear an
|
|
1581
|
+
// old terminal meaning rather than rendering contradictory status/reason pairs.
|
|
1582
|
+
next.resolution_kind = null;
|
|
1583
|
+
}
|
|
1356
1584
|
const openToTerminal = existing.status !== 'done' &&
|
|
1357
1585
|
existing.status !== 'cancelled' &&
|
|
1358
1586
|
(next.status === 'done' || next.status === 'cancelled');
|
|
@@ -1380,6 +1608,8 @@ class TaskLedger {
|
|
|
1380
1608
|
'last_temporal_attempt_id',
|
|
1381
1609
|
'review_started_at',
|
|
1382
1610
|
'review_anchor_event_id',
|
|
1611
|
+
'completion_criteria',
|
|
1612
|
+
'resolution_kind',
|
|
1383
1613
|
];
|
|
1384
1614
|
const changedColumns = persistedColumns.filter((column) => next[column] !== existing[column]);
|
|
1385
1615
|
if (changedColumns.length === 0) {
|
|
@@ -1424,7 +1654,8 @@ class TaskLedger {
|
|
|
1424
1654
|
assertCandidateTaskMutationAllowed(taskId, patch, origin) {
|
|
1425
1655
|
if (origin.workOrderAttemptId === undefined ||
|
|
1426
1656
|
(!Object.prototype.hasOwnProperty.call(patch, 'status') &&
|
|
1427
|
-
!Object.prototype.hasOwnProperty.call(patch, 'latest_event')
|
|
1657
|
+
!Object.prototype.hasOwnProperty.call(patch, 'latest_event') &&
|
|
1658
|
+
!Object.prototype.hasOwnProperty.call(patch, 'completion_criteria'))) {
|
|
1428
1659
|
return;
|
|
1429
1660
|
}
|
|
1430
1661
|
const attempt = this.getWorkOrderById(origin.workOrderAttemptId);
|
|
@@ -1439,6 +1670,18 @@ class TaskLedger {
|
|
|
1439
1670
|
}
|
|
1440
1671
|
const { attempts: _attempts, ...payload } = attempt.payload;
|
|
1441
1672
|
(0, workorder_publishers_js_1.validateWorkOrderPayload)('board', payload);
|
|
1673
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'completion_criteria') &&
|
|
1674
|
+
!origin.taskCreateUpsert) {
|
|
1675
|
+
const reclassificationCandidates = Array.isArray(payload.reclassificationCandidates)
|
|
1676
|
+
? payload.reclassificationCandidates
|
|
1677
|
+
: [];
|
|
1678
|
+
const qualificationCandidate = reclassificationCandidates.find((candidate) => candidate.taskId === taskId);
|
|
1679
|
+
if (!qualificationCandidate ||
|
|
1680
|
+
(patch.expected_revision !== undefined &&
|
|
1681
|
+
qualificationCandidate.taskRevision !== patch.expected_revision)) {
|
|
1682
|
+
throw new Error(`task_update: task ${taskId} is outside this host-issued qualification candidate set`);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1442
1685
|
if (payload.mode !== 'reconcile' || !payload.candidates) {
|
|
1443
1686
|
return;
|
|
1444
1687
|
}
|
|
@@ -1546,6 +1789,40 @@ class TaskLedger {
|
|
|
1546
1789
|
completedAt: row.updated_at,
|
|
1547
1790
|
};
|
|
1548
1791
|
}
|
|
1792
|
+
/**
|
|
1793
|
+
* The newest COMPLETED wiki run - the baseline the wiki daily-continuity
|
|
1794
|
+
* planner gates against (wiki-continuity.ts). Only `status='done'` rows count.
|
|
1795
|
+
*
|
|
1796
|
+
* `ownerDate`/`sourceWatermark` are read straight from the completed run's
|
|
1797
|
+
* payload and are null for a LEGACY row (one enqueued before the continuity
|
|
1798
|
+
* fields existed): the planner treats a null-field baseline as "no baseline"
|
|
1799
|
+
* and re-runs from the owner day's start rather than fabricating a boundary.
|
|
1800
|
+
*/
|
|
1801
|
+
lastCompletedWikiRun() {
|
|
1802
|
+
const row = this.db
|
|
1803
|
+
.prepare(`SELECT json_extract(payload, '$.ownerDate') AS owner_date,
|
|
1804
|
+
json_extract(payload, '$.sourceWatermark') AS source_watermark,
|
|
1805
|
+
json_extract(payload, '$.range.end_ms') AS covered_through_ms,
|
|
1806
|
+
updated_at
|
|
1807
|
+
FROM operator_tasks
|
|
1808
|
+
WHERE kind = 'system' AND source_channel = ? AND status = 'done'
|
|
1809
|
+
ORDER BY id DESC LIMIT 1`)
|
|
1810
|
+
.get(`${exports.WORKORDER_CHANNEL_PREFIX}wiki`);
|
|
1811
|
+
if (!row)
|
|
1812
|
+
return null;
|
|
1813
|
+
return {
|
|
1814
|
+
ownerDate: typeof row.owner_date === 'string' && row.owner_date.length > 0 ? row.owner_date : null,
|
|
1815
|
+
sourceWatermark: typeof row.source_watermark === 'string' && row.source_watermark.length > 0
|
|
1816
|
+
? row.source_watermark
|
|
1817
|
+
: null,
|
|
1818
|
+
// The prior run's range.end_ms - where the next same-day run resumes.
|
|
1819
|
+
// Null for a legacy row with no range; never fabricated.
|
|
1820
|
+
coveredThroughMs: typeof row.covered_through_ms === 'number' && Number.isFinite(row.covered_through_ms)
|
|
1821
|
+
? row.covered_through_ms
|
|
1822
|
+
: null,
|
|
1823
|
+
completedAt: row.updated_at,
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1549
1826
|
/**
|
|
1550
1827
|
* Board-input term for the NATIVE owner task ledger - the pipeline slot's
|
|
1551
1828
|
* projection source. Reuses payloadHash(): it already digests exactly the
|