@jungjaehoon/mama-os 0.48.0 → 0.48.1
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/code-act/host-bridge.d.ts.map +1 -1
- package/dist/agent/code-act/host-bridge.js +40 -4
- 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 +94 -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 +1 -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 +17 -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 +84 -1
- package/dist/operator/task-ledger.d.ts.map +1 -1
- package/dist/operator/task-ledger.js +281 -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 +22 -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 +149 -0
- package/dist/operator/wiki-continuity.d.ts.map +1 -0
- package/dist/operator/wiki-continuity.js +214 -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 +122 -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,12 @@ class TaskLedger {
|
|
|
377
418
|
where.push('updated_at >= ?');
|
|
378
419
|
params.push(filter.updatedSinceMs);
|
|
379
420
|
}
|
|
421
|
+
if (filter.qualification === 'qualified') {
|
|
422
|
+
where.push('completion_criteria IS NOT NULL');
|
|
423
|
+
}
|
|
424
|
+
else if (filter.qualification === 'legacy_unqualified') {
|
|
425
|
+
where.push('completion_criteria IS NULL');
|
|
426
|
+
}
|
|
380
427
|
return { where, params };
|
|
381
428
|
}
|
|
382
429
|
/** ORDER BY for both list() and listPage(); the trailing id makes it a TOTAL order,
|
|
@@ -873,7 +920,15 @@ class TaskLedger {
|
|
|
873
920
|
outcome = 'superseded';
|
|
874
921
|
}
|
|
875
922
|
else if (input.decision === 'apply') {
|
|
876
|
-
const
|
|
923
|
+
const terminalEvidence = candidate.proposedStatus === 'done' || candidate.proposedStatus === 'cancelled';
|
|
924
|
+
const transitioned = this.transitionTaskInTransaction(candidate.taskId, {
|
|
925
|
+
status: candidate.proposedStatus,
|
|
926
|
+
latest_event: candidate.evidenceSummary,
|
|
927
|
+
// This candidate is a host-issued current authoritative lifecycle
|
|
928
|
+
// observation. Preserve its semantic completion, or clear an earlier
|
|
929
|
+
// terminal classification when later feedback reopens the same row.
|
|
930
|
+
resolution_kind: terminalEvidence ? 'completed_evidence' : null,
|
|
931
|
+
}, origin, true);
|
|
877
932
|
taskRevisionAfter = transitioned.revision;
|
|
878
933
|
outcome = 'applied';
|
|
879
934
|
}
|
|
@@ -1053,6 +1108,9 @@ class TaskLedger {
|
|
|
1053
1108
|
}
|
|
1054
1109
|
const normalizedDeadline = exactDue?.deadline ?? input.deadline ?? null;
|
|
1055
1110
|
const initialTemporalEpoch = normalizedDeadline !== null ? 1 : 0;
|
|
1111
|
+
// Optional HERE on purpose: the PUBLIC task_create boundary requires it, while
|
|
1112
|
+
// internal/system seeding and legacy callers stay backward compatible.
|
|
1113
|
+
const normalizedCompletionCriteria = normalizeCompletionCriteria(input.completion_criteria, 'task_create');
|
|
1056
1114
|
const now = this.now();
|
|
1057
1115
|
if (input.source_channel && input.source_event_id) {
|
|
1058
1116
|
// kind='owner' probe: an agent-supplied (channel, event) pair can never
|
|
@@ -1073,6 +1131,9 @@ class TaskLedger {
|
|
|
1073
1131
|
...(input.due_at !== undefined ? { due_at: input.due_at } : {}),
|
|
1074
1132
|
...(input.confirmed !== undefined ? { confirmed: input.confirmed } : {}),
|
|
1075
1133
|
...(input.latest_event !== undefined ? { latest_event: input.latest_event } : {}),
|
|
1134
|
+
...(normalizedCompletionCriteria !== null
|
|
1135
|
+
? { completion_criteria: normalizedCompletionCriteria }
|
|
1136
|
+
: {}),
|
|
1076
1137
|
...(input.expected_revision !== undefined
|
|
1077
1138
|
? { expected_revision: input.expected_revision }
|
|
1078
1139
|
: {}),
|
|
@@ -1084,6 +1145,7 @@ class TaskLedger {
|
|
|
1084
1145
|
// carried nothing.
|
|
1085
1146
|
{
|
|
1086
1147
|
...origin,
|
|
1148
|
+
taskCreateUpsert: true,
|
|
1087
1149
|
causeEventIds: origin.causeEventIds?.length
|
|
1088
1150
|
? origin.causeEventIds
|
|
1089
1151
|
: input.source_event_id
|
|
@@ -1099,9 +1161,9 @@ class TaskLedger {
|
|
|
1099
1161
|
.prepare(`INSERT INTO operator_tasks
|
|
1100
1162
|
(title, status, priority, assignee, deadline, due_at, deadline_offset_minutes,
|
|
1101
1163
|
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);
|
|
1164
|
+
auto_created, confirmed, completion_criteria, created_at, updated_at)
|
|
1165
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1166
|
+
.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
1167
|
createdId = Number(result.lastInsertRowid);
|
|
1106
1168
|
this.recordTaskChange({
|
|
1107
1169
|
kind: 'task_create',
|
|
@@ -1142,6 +1204,7 @@ class TaskLedger {
|
|
|
1142
1204
|
source_event_id: input.source_event_id ?? null,
|
|
1143
1205
|
latest_event: input.latest_event ?? null,
|
|
1144
1206
|
confirmed: input.confirmed ? 1 : 0,
|
|
1207
|
+
completion_criteria: normalizedCompletionCriteria,
|
|
1145
1208
|
},
|
|
1146
1209
|
atMs: now,
|
|
1147
1210
|
});
|
|
@@ -1203,6 +1266,148 @@ class TaskLedger {
|
|
|
1203
1266
|
throw error;
|
|
1204
1267
|
}
|
|
1205
1268
|
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Recorrect an existing owner row with a NAMED disposition.
|
|
1271
|
+
*
|
|
1272
|
+
* Records and tasks are separate. Without a semantic terminal reason the agent
|
|
1273
|
+
* could not distinguish completed work from an item that was never a task, so
|
|
1274
|
+
* connector observations, principles, lessons and open questions accumulated as
|
|
1275
|
+
* owner rows and could only be "closed" as if they had been done.
|
|
1276
|
+
*
|
|
1277
|
+
* Every path requires the exact current revision, a non-empty bounded reason
|
|
1278
|
+
* preserved as `latest_event`, an OWNER row, and the same candidate/workorder
|
|
1279
|
+
* authority checks as task_update (TG-03/TG-04/TG-06) - a terminal or foreign
|
|
1280
|
+
* board attempt cannot mutate an unrelated row. The whole transition, its
|
|
1281
|
+
* `resolution_kind` and its effect receipt commit as ONE transaction.
|
|
1282
|
+
*/
|
|
1283
|
+
reclassify(id, input, origin = {}) {
|
|
1284
|
+
const disposition = input?.disposition;
|
|
1285
|
+
if (!exports.TASK_RECLASSIFY_DISPOSITIONS.includes(disposition)) {
|
|
1286
|
+
throw new Error(`task_reclassify: disposition must be one of ${exports.TASK_RECLASSIFY_DISPOSITIONS.join('|')}, got: ${String(disposition)}`);
|
|
1287
|
+
}
|
|
1288
|
+
const reason = typeof input.reason === 'string' ? input.reason.trim() : '';
|
|
1289
|
+
if (reason.length === 0) {
|
|
1290
|
+
throw new Error('task_reclassify: reason must be a non-empty string');
|
|
1291
|
+
}
|
|
1292
|
+
if (reason.length > exports.TASK_RECLASSIFY_REASON_MAX_LENGTH) {
|
|
1293
|
+
throw new Error(`task_reclassify: reason must be at most ${exports.TASK_RECLASSIFY_REASON_MAX_LENGTH} characters`);
|
|
1294
|
+
}
|
|
1295
|
+
if (!Number.isSafeInteger(input.expected_revision) || input.expected_revision < 0) {
|
|
1296
|
+
throw new Error('task_reclassify: expected_revision must be a non-negative integer');
|
|
1297
|
+
}
|
|
1298
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
1299
|
+
try {
|
|
1300
|
+
const existing = this.db.prepare('SELECT * FROM operator_tasks WHERE id = ?').get(id);
|
|
1301
|
+
if (!existing)
|
|
1302
|
+
throw new Error(`task_reclassify: no task with id ${id}`);
|
|
1303
|
+
if (existing.kind === 'system') {
|
|
1304
|
+
throw new Error(`task_reclassify: task ${id} is a system workorder row (owner rows only)`);
|
|
1305
|
+
}
|
|
1306
|
+
if (input.expected_revision !== existing.revision) {
|
|
1307
|
+
throw new Error(`task_reclassify: expected revision ${input.expected_revision}, current ${existing.revision}`);
|
|
1308
|
+
}
|
|
1309
|
+
this.assertTaskReclassificationAuthorized(existing, origin);
|
|
1310
|
+
const patch = this.buildReclassifyPatch(existing, disposition, reason);
|
|
1311
|
+
// Same primitive as task_update: candidate guard, board-attempt liveness,
|
|
1312
|
+
// revision-checked UPDATE, effect receipt, temporal generation supersession.
|
|
1313
|
+
const record = this.transitionTaskInTransaction(id, patch, origin);
|
|
1314
|
+
this.db.exec('COMMIT');
|
|
1315
|
+
return record;
|
|
1316
|
+
}
|
|
1317
|
+
catch (error) {
|
|
1318
|
+
this.db.exec('ROLLBACK');
|
|
1319
|
+
throw error;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
assertTaskReclassificationAuthorized(existing, origin) {
|
|
1323
|
+
if (origin.workOrderAttemptId !== undefined) {
|
|
1324
|
+
const attempt = this.getWorkOrderById(origin.workOrderAttemptId);
|
|
1325
|
+
if (!attempt || attempt.workKind !== 'board' || attempt.status !== 'in_progress') {
|
|
1326
|
+
throw new Error(`task_reclassify: Board workorder ${String(origin.workOrderAttemptId)} is no longer active`);
|
|
1327
|
+
}
|
|
1328
|
+
const { attempts: _attempts, ...payload } = attempt.payload;
|
|
1329
|
+
(0, workorder_publishers_js_1.validateWorkOrderPayload)('board', payload);
|
|
1330
|
+
const candidates = Array.isArray(payload.reclassificationCandidates)
|
|
1331
|
+
? payload.reclassificationCandidates
|
|
1332
|
+
: [];
|
|
1333
|
+
const candidate = candidates.find((item) => item.taskId === existing.id);
|
|
1334
|
+
if (!candidate || candidate.taskRevision !== existing.revision) {
|
|
1335
|
+
throw new Error(`task_reclassify: task ${existing.id} is outside this host-issued Board candidate set`);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (origin.reclassificationCauseBound) {
|
|
1339
|
+
const eventIds = (origin.causeEventIds ?? []).filter(effects_js_1.isUsableCause);
|
|
1340
|
+
if (eventIds.length === 0) {
|
|
1341
|
+
throw new Error('task_reclassify: owner-event reclassification requires causal events');
|
|
1342
|
+
}
|
|
1343
|
+
if (existing.source_event_id && eventIds.includes(existing.source_event_id))
|
|
1344
|
+
return;
|
|
1345
|
+
const placeholders = eventIds.map(() => '?').join(',');
|
|
1346
|
+
const channels = this.db
|
|
1347
|
+
.prepare(`SELECT DISTINCT inbox.channel_key
|
|
1348
|
+
FROM owner_event_inbox AS inbox, json_each(inbox.event_ids_json) AS event
|
|
1349
|
+
WHERE event.value IN (${placeholders})`)
|
|
1350
|
+
.all(...eventIds);
|
|
1351
|
+
if (existing.source_channel === null ||
|
|
1352
|
+
!channels.some((row) => row.channel_key === existing.source_channel)) {
|
|
1353
|
+
throw new Error(`task_reclassify: task ${existing.id} is outside this owner-event source channel`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
buildReclassifyPatch(existing, disposition, reason) {
|
|
1358
|
+
const base = {
|
|
1359
|
+
latest_event: reason,
|
|
1360
|
+
expected_revision: existing.revision,
|
|
1361
|
+
};
|
|
1362
|
+
if (disposition === 'reopen') {
|
|
1363
|
+
if (existing.status !== 'done' && existing.status !== 'cancelled') {
|
|
1364
|
+
throw new Error(`task_reclassify: reopen requires a terminal owner row, task is '${existing.status}'`);
|
|
1365
|
+
}
|
|
1366
|
+
// A reopened row must not inherit a deadline that already passed: it would
|
|
1367
|
+
// read as overdue the moment it reopens. A still-future deadline is kept.
|
|
1368
|
+
const stale = this.isDeadlineInThePast(existing);
|
|
1369
|
+
return {
|
|
1370
|
+
...base,
|
|
1371
|
+
status: 'pending',
|
|
1372
|
+
resolution_kind: null,
|
|
1373
|
+
...(stale ? { deadline: null, due_at: null } : {}),
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
if (existing.status === 'done' || existing.status === 'cancelled') {
|
|
1377
|
+
throw new Error(`task_reclassify: ${disposition} requires an active owner row, task is '${existing.status}'`);
|
|
1378
|
+
}
|
|
1379
|
+
if (disposition === 'completed_evidence') {
|
|
1380
|
+
return { ...base, status: 'done', resolution_kind: 'completed_evidence' };
|
|
1381
|
+
}
|
|
1382
|
+
if (disposition === 'completed_no_issue') {
|
|
1383
|
+
// Owner policy: a PAST deadline plus a complete relevant-source check with no
|
|
1384
|
+
// issue is enough. Without a passed deadline there is nothing to conclude from.
|
|
1385
|
+
if (!this.isDeadlineInThePast(existing)) {
|
|
1386
|
+
throw new Error('task_reclassify: completed_no_issue requires a deadline or due_at already in the past');
|
|
1387
|
+
}
|
|
1388
|
+
return { ...base, status: 'done', resolution_kind: 'completed_no_issue' };
|
|
1389
|
+
}
|
|
1390
|
+
// non_task_record / non_task_memory: this was never a task. Cancelled, not done,
|
|
1391
|
+
// so accounting never reads it as finished work.
|
|
1392
|
+
return {
|
|
1393
|
+
...base,
|
|
1394
|
+
status: 'cancelled',
|
|
1395
|
+
completion_criteria: null,
|
|
1396
|
+
resolution_kind: disposition,
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
/** True when the row carries a deadline/due_at that has already passed. */
|
|
1400
|
+
isDeadlineInThePast(existing) {
|
|
1401
|
+
const state = (0, task_temporal_js_1.deriveTemporalState)({
|
|
1402
|
+
// Terminal rows derive 'closed', which would hide a genuinely past deadline;
|
|
1403
|
+
// ask the temporal question about the CLOCK fields only.
|
|
1404
|
+
status: 'pending',
|
|
1405
|
+
dueAt: existing.due_at,
|
|
1406
|
+
deadlineIso: existing.deadline,
|
|
1407
|
+
deadlineOffsetMinutes: existing.deadline_offset_minutes,
|
|
1408
|
+
}, this.now(), this.timeZone);
|
|
1409
|
+
return state === 'exact_overdue' || state === 'date_overdue';
|
|
1410
|
+
}
|
|
1206
1411
|
/**
|
|
1207
1412
|
* The one mutation primitive for owner-task state. Callers own the outer
|
|
1208
1413
|
* transaction so lifecycle receipts, effects, generation ownership, and the
|
|
@@ -1224,7 +1429,8 @@ class TaskLedger {
|
|
|
1224
1429
|
: null;
|
|
1225
1430
|
const hasLifecyclePatch = Object.prototype.hasOwnProperty.call(patch, 'status') ||
|
|
1226
1431
|
Object.prototype.hasOwnProperty.call(patch, 'due_at') ||
|
|
1227
|
-
Object.prototype.hasOwnProperty.call(patch, 'latest_event')
|
|
1432
|
+
Object.prototype.hasOwnProperty.call(patch, 'latest_event') ||
|
|
1433
|
+
Object.prototype.hasOwnProperty.call(patch, 'completion_criteria');
|
|
1228
1434
|
const isBoardWorkorderLifecycleJudgment = revisionAttempt?.workKind === 'board' && hasLifecyclePatch;
|
|
1229
1435
|
if (isBoardWorkorderLifecycleJudgment && revisionAttempt.status !== 'in_progress') {
|
|
1230
1436
|
throw new Error(`task_update: board workorder ${revisionAttempt.id} is no longer active (${revisionAttempt.status})`);
|
|
@@ -1276,6 +1482,8 @@ class TaskLedger {
|
|
|
1276
1482
|
last_temporal_attempt_id: existing.last_temporal_attempt_id,
|
|
1277
1483
|
review_started_at: existing.review_started_at,
|
|
1278
1484
|
review_anchor_event_id: existing.review_anchor_event_id,
|
|
1485
|
+
completion_criteria: existing.completion_criteria,
|
|
1486
|
+
resolution_kind: existing.resolution_kind,
|
|
1279
1487
|
};
|
|
1280
1488
|
if (patch.title !== undefined)
|
|
1281
1489
|
next.title = patch.title.trim();
|
|
@@ -1289,6 +1497,12 @@ class TaskLedger {
|
|
|
1289
1497
|
next.latest_event = patch.latest_event;
|
|
1290
1498
|
if (patch.confirmed !== undefined)
|
|
1291
1499
|
next.confirmed = patch.confirmed ? 1 : 0;
|
|
1500
|
+
if (patch.completion_criteria !== undefined) {
|
|
1501
|
+
next.completion_criteria = normalizeCompletionCriteria(patch.completion_criteria, 'task_update');
|
|
1502
|
+
}
|
|
1503
|
+
// Host-internal (reclassify only); the public task_update surface rejects it.
|
|
1504
|
+
if (patch.resolution_kind !== undefined)
|
|
1505
|
+
next.resolution_kind = patch.resolution_kind;
|
|
1292
1506
|
const leavingVerifiedReview = existing.status === 'review' &&
|
|
1293
1507
|
patch.status !== undefined &&
|
|
1294
1508
|
patch.status !== 'review' &&
|
|
@@ -1353,6 +1567,16 @@ class TaskLedger {
|
|
|
1353
1567
|
const terminalToOpen = (existing.status === 'done' || existing.status === 'cancelled') &&
|
|
1354
1568
|
next.status !== 'done' &&
|
|
1355
1569
|
next.status !== 'cancelled';
|
|
1570
|
+
if (terminalToOpen && existing.resolution_kind !== null && patch.resolution_kind !== null) {
|
|
1571
|
+
throw new Error('task_update: a classified terminal row must reopen through task_reclassify');
|
|
1572
|
+
}
|
|
1573
|
+
if (patch.status !== undefined &&
|
|
1574
|
+
next.status !== existing.status &&
|
|
1575
|
+
patch.resolution_kind === undefined) {
|
|
1576
|
+
// A generic status change has no semantic reclassification authority. Clear an
|
|
1577
|
+
// old terminal meaning rather than rendering contradictory status/reason pairs.
|
|
1578
|
+
next.resolution_kind = null;
|
|
1579
|
+
}
|
|
1356
1580
|
const openToTerminal = existing.status !== 'done' &&
|
|
1357
1581
|
existing.status !== 'cancelled' &&
|
|
1358
1582
|
(next.status === 'done' || next.status === 'cancelled');
|
|
@@ -1380,6 +1604,8 @@ class TaskLedger {
|
|
|
1380
1604
|
'last_temporal_attempt_id',
|
|
1381
1605
|
'review_started_at',
|
|
1382
1606
|
'review_anchor_event_id',
|
|
1607
|
+
'completion_criteria',
|
|
1608
|
+
'resolution_kind',
|
|
1383
1609
|
];
|
|
1384
1610
|
const changedColumns = persistedColumns.filter((column) => next[column] !== existing[column]);
|
|
1385
1611
|
if (changedColumns.length === 0) {
|
|
@@ -1424,7 +1650,8 @@ class TaskLedger {
|
|
|
1424
1650
|
assertCandidateTaskMutationAllowed(taskId, patch, origin) {
|
|
1425
1651
|
if (origin.workOrderAttemptId === undefined ||
|
|
1426
1652
|
(!Object.prototype.hasOwnProperty.call(patch, 'status') &&
|
|
1427
|
-
!Object.prototype.hasOwnProperty.call(patch, 'latest_event')
|
|
1653
|
+
!Object.prototype.hasOwnProperty.call(patch, 'latest_event') &&
|
|
1654
|
+
!Object.prototype.hasOwnProperty.call(patch, 'completion_criteria'))) {
|
|
1428
1655
|
return;
|
|
1429
1656
|
}
|
|
1430
1657
|
const attempt = this.getWorkOrderById(origin.workOrderAttemptId);
|
|
@@ -1439,6 +1666,18 @@ class TaskLedger {
|
|
|
1439
1666
|
}
|
|
1440
1667
|
const { attempts: _attempts, ...payload } = attempt.payload;
|
|
1441
1668
|
(0, workorder_publishers_js_1.validateWorkOrderPayload)('board', payload);
|
|
1669
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'completion_criteria') &&
|
|
1670
|
+
!origin.taskCreateUpsert) {
|
|
1671
|
+
const reclassificationCandidates = Array.isArray(payload.reclassificationCandidates)
|
|
1672
|
+
? payload.reclassificationCandidates
|
|
1673
|
+
: [];
|
|
1674
|
+
const qualificationCandidate = reclassificationCandidates.find((candidate) => candidate.taskId === taskId);
|
|
1675
|
+
if (!qualificationCandidate ||
|
|
1676
|
+
(patch.expected_revision !== undefined &&
|
|
1677
|
+
qualificationCandidate.taskRevision !== patch.expected_revision)) {
|
|
1678
|
+
throw new Error(`task_update: task ${taskId} is outside this host-issued qualification candidate set`);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1442
1681
|
if (payload.mode !== 'reconcile' || !payload.candidates) {
|
|
1443
1682
|
return;
|
|
1444
1683
|
}
|
|
@@ -1546,6 +1785,40 @@ class TaskLedger {
|
|
|
1546
1785
|
completedAt: row.updated_at,
|
|
1547
1786
|
};
|
|
1548
1787
|
}
|
|
1788
|
+
/**
|
|
1789
|
+
* The newest COMPLETED wiki run - the baseline the wiki daily-continuity
|
|
1790
|
+
* planner gates against (wiki-continuity.ts). Only `status='done'` rows count.
|
|
1791
|
+
*
|
|
1792
|
+
* `ownerDate`/`sourceWatermark` are read straight from the completed run's
|
|
1793
|
+
* payload and are null for a LEGACY row (one enqueued before the continuity
|
|
1794
|
+
* fields existed): the planner treats a null-field baseline as "no baseline"
|
|
1795
|
+
* and re-runs from the owner day's start rather than fabricating a boundary.
|
|
1796
|
+
*/
|
|
1797
|
+
lastCompletedWikiRun() {
|
|
1798
|
+
const row = this.db
|
|
1799
|
+
.prepare(`SELECT json_extract(payload, '$.ownerDate') AS owner_date,
|
|
1800
|
+
json_extract(payload, '$.sourceWatermark') AS source_watermark,
|
|
1801
|
+
json_extract(payload, '$.range.end_ms') AS covered_through_ms,
|
|
1802
|
+
updated_at
|
|
1803
|
+
FROM operator_tasks
|
|
1804
|
+
WHERE kind = 'system' AND source_channel = ? AND status = 'done'
|
|
1805
|
+
ORDER BY id DESC LIMIT 1`)
|
|
1806
|
+
.get(`${exports.WORKORDER_CHANNEL_PREFIX}wiki`);
|
|
1807
|
+
if (!row)
|
|
1808
|
+
return null;
|
|
1809
|
+
return {
|
|
1810
|
+
ownerDate: typeof row.owner_date === 'string' && row.owner_date.length > 0 ? row.owner_date : null,
|
|
1811
|
+
sourceWatermark: typeof row.source_watermark === 'string' && row.source_watermark.length > 0
|
|
1812
|
+
? row.source_watermark
|
|
1813
|
+
: null,
|
|
1814
|
+
// The prior run's range.end_ms - where the next same-day run resumes.
|
|
1815
|
+
// Null for a legacy row with no range; never fabricated.
|
|
1816
|
+
coveredThroughMs: typeof row.covered_through_ms === 'number' && Number.isFinite(row.covered_through_ms)
|
|
1817
|
+
? row.covered_through_ms
|
|
1818
|
+
: null,
|
|
1819
|
+
completedAt: row.updated_at,
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1549
1822
|
/**
|
|
1550
1823
|
* Board-input term for the NATIVE owner task ledger - the pipeline slot's
|
|
1551
1824
|
* projection source. Reuses payloadHash(): it already digests exactly the
|