@jungjaehoon/mama-os 0.49.2 → 0.50.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/dist/agent/agent-loop.d.ts +5 -1
- package/dist/agent/agent-loop.d.ts.map +1 -1
- package/dist/agent/agent-loop.js +24 -4
- 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 +39 -7
- package/dist/agent/code-act/host-bridge.js.map +1 -1
- package/dist/agent/codex-app-server-process.d.ts.map +1 -1
- package/dist/agent/codex-app-server-process.js +37 -0
- package/dist/agent/codex-app-server-process.js.map +1 -1
- package/dist/agent/gateway-tool-executor.d.ts +17 -6
- package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
- package/dist/agent/gateway-tool-executor.js +512 -143
- package/dist/agent/gateway-tool-executor.js.map +1 -1
- package/dist/agent/gateway-tools.md +7 -6
- package/dist/agent/native-effect-observer.d.ts +19 -0
- package/dist/agent/native-effect-observer.d.ts.map +1 -0
- package/dist/agent/native-effect-observer.js +70 -0
- package/dist/agent/native-effect-observer.js.map +1 -0
- package/dist/agent/persistent-cli-process.d.ts.map +1 -1
- package/dist/agent/persistent-cli-process.js +6 -1
- package/dist/agent/persistent-cli-process.js.map +1 -1
- package/dist/agent/tool-registry.d.ts.map +1 -1
- package/dist/agent/tool-registry.js +15 -8
- package/dist/agent/tool-registry.js.map +1 -1
- package/dist/agent/types.d.ts +8 -2
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/api/report-handler.d.ts +19 -3
- package/dist/api/report-handler.d.ts.map +1 -1
- package/dist/api/report-handler.js +123 -17
- package/dist/api/report-handler.js.map +1 -1
- package/dist/api/report-persistence.d.ts +3 -3
- package/dist/api/report-persistence.d.ts.map +1 -1
- package/dist/api/report-persistence.js +11 -21
- package/dist/api/report-persistence.js.map +1 -1
- package/dist/cli/commands/start.d.ts +4 -0
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +49 -157
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/runtime/agent-loop-init.d.ts.map +1 -1
- package/dist/cli/runtime/agent-loop-init.js +1 -0
- package/dist/cli/runtime/agent-loop-init.js.map +1 -1
- package/dist/cli/runtime/api-routes-init.d.ts +5 -19
- package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
- package/dist/cli/runtime/api-routes-init.js +7 -79
- package/dist/cli/runtime/api-routes-init.js.map +1 -1
- package/dist/db/migrations/operator-owner-action-candidates.d.ts +19 -0
- package/dist/db/migrations/operator-owner-action-candidates.d.ts.map +1 -0
- package/dist/db/migrations/operator-owner-action-candidates.js +254 -0
- package/dist/db/migrations/operator-owner-action-candidates.js.map +1 -0
- package/dist/db/migrations/operator-task-external-lifecycle.d.ts +9 -0
- package/dist/db/migrations/operator-task-external-lifecycle.d.ts.map +1 -1
- package/dist/db/migrations/operator-task-external-lifecycle.js +52 -22
- package/dist/db/migrations/operator-task-external-lifecycle.js.map +1 -1
- package/dist/db/migrations/operator-task-review-lifecycle.d.ts.map +1 -1
- package/dist/db/migrations/operator-task-review-lifecycle.js +3 -0
- package/dist/db/migrations/operator-task-review-lifecycle.js.map +1 -1
- package/dist/db/migrations/owner-action-effects.d.ts +18 -0
- package/dist/db/migrations/owner-action-effects.d.ts.map +1 -0
- package/dist/db/migrations/owner-action-effects.js +111 -0
- package/dist/db/migrations/owner-action-effects.js.map +1 -0
- package/dist/gateways/message-router.d.ts.map +1 -1
- package/dist/gateways/message-router.js +14 -4
- package/dist/gateways/message-router.js.map +1 -1
- package/dist/gateways/telegram-text-entities.d.ts +99 -0
- package/dist/gateways/telegram-text-entities.d.ts.map +1 -0
- package/dist/gateways/telegram-text-entities.js +189 -0
- package/dist/gateways/telegram-text-entities.js.map +1 -0
- package/dist/gateways/telegram.d.ts.map +1 -1
- package/dist/gateways/telegram.js +13 -2
- package/dist/gateways/telegram.js.map +1 -1
- package/dist/gateways/types.d.ts +8 -0
- package/dist/gateways/types.d.ts.map +1 -1
- package/dist/operator/board-read-views.d.ts +9 -4
- package/dist/operator/board-read-views.d.ts.map +1 -1
- package/dist/operator/board-read-views.js +11 -0
- package/dist/operator/board-read-views.js.map +1 -1
- package/dist/operator/external-lifecycle-candidates.d.ts +15 -1
- package/dist/operator/external-lifecycle-candidates.d.ts.map +1 -1
- package/dist/operator/external-lifecycle-candidates.js +164 -0
- package/dist/operator/external-lifecycle-candidates.js.map +1 -1
- package/dist/operator/external-lifecycle-discovery.d.ts +61 -0
- package/dist/operator/external-lifecycle-discovery.d.ts.map +1 -0
- package/dist/operator/external-lifecycle-discovery.js +103 -0
- package/dist/operator/external-lifecycle-discovery.js.map +1 -0
- package/dist/operator/external-lifecycle.d.ts +23 -0
- package/dist/operator/external-lifecycle.d.ts.map +1 -1
- package/dist/operator/owner-action-effects.d.ts +101 -0
- package/dist/operator/owner-action-effects.d.ts.map +1 -0
- package/dist/operator/owner-action-effects.js +335 -0
- package/dist/operator/owner-action-effects.js.map +1 -0
- package/dist/operator/owner-event-inbox.d.ts +1 -0
- package/dist/operator/owner-event-inbox.d.ts.map +1 -1
- package/dist/operator/owner-event-inbox.js +6 -0
- package/dist/operator/owner-event-inbox.js.map +1 -1
- package/dist/operator/owner-event-loop.d.ts +3 -0
- package/dist/operator/owner-event-loop.d.ts.map +1 -1
- package/dist/operator/owner-event-loop.js +34 -0
- package/dist/operator/owner-event-loop.js.map +1 -1
- package/dist/operator/owner-event-outcome.d.ts.map +1 -1
- package/dist/operator/owner-event-outcome.js +6 -0
- 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 +2 -26
- 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 +2 -5
- package/dist/operator/owner-event-prompt.js.map +1 -1
- package/dist/operator/task-ledger.d.ts +93 -14
- package/dist/operator/task-ledger.d.ts.map +1 -1
- package/dist/operator/task-ledger.js +430 -103
- package/dist/operator/task-ledger.js.map +1 -1
- package/dist/operator/temporal-reconcile.d.ts.map +1 -1
- package/dist/operator/temporal-reconcile.js +6 -2
- package/dist/operator/temporal-reconcile.js.map +1 -1
- package/dist/operator/workorder-consumer.d.ts +2 -0
- package/dist/operator/workorder-consumer.d.ts.map +1 -1
- package/dist/operator/workorder-consumer.js +20 -5
- package/dist/operator/workorder-consumer.js.map +1 -1
- package/dist/scheduler/heartbeat.d.ts.map +1 -1
- package/dist/scheduler/heartbeat.js +1 -0
- package/dist/scheduler/heartbeat.js.map +1 -1
- package/dist/sqlite.d.ts +1 -1
- package/dist/sqlite.d.ts.map +1 -1
- package/dist/sqlite.js +3 -2
- package/dist/sqlite.js.map +1 -1
- package/dist/wiki/wiki-read.d.ts +2 -1
- package/dist/wiki/wiki-read.d.ts.map +1 -1
- package/dist/wiki/wiki-read.js +13 -2
- package/dist/wiki/wiki-read.js.map +1 -1
- package/dist/wiki/wiki-turn-contract.js +1 -1
- package/dist/wiki/wiki-turn-contract.js.map +1 -1
- package/package.json +1 -1
- package/public/viewer/operator/operator.js +3 -1
|
@@ -27,6 +27,7 @@ const operator_task_temporal_js_1 = require("../db/migrations/operator-task-temp
|
|
|
27
27
|
const operator_task_external_lifecycle_js_1 = require("../db/migrations/operator-task-external-lifecycle.js");
|
|
28
28
|
const operator_task_review_lifecycle_js_1 = require("../db/migrations/operator-task-review-lifecycle.js");
|
|
29
29
|
const operator_task_classification_js_1 = require("../db/migrations/operator-task-classification.js");
|
|
30
|
+
const operator_owner_action_candidates_js_1 = require("../db/migrations/operator-owner-action-candidates.js");
|
|
30
31
|
const effects_js_1 = require("../evidence/effects.js");
|
|
31
32
|
const temporal_effect_js_1 = require("./temporal-effect.js");
|
|
32
33
|
const task_temporal_js_1 = require("./task-temporal.js");
|
|
@@ -129,6 +130,40 @@ function isoToEpochMs(iso) {
|
|
|
129
130
|
const ms = Date.parse(`${iso}T00:00:00Z`);
|
|
130
131
|
return Number.isNaN(ms) ? null : ms;
|
|
131
132
|
}
|
|
133
|
+
const OWNER_ACTION_IDENTITY_MAX_LENGTH = 512;
|
|
134
|
+
function requireOwnerActionIdentity(value, field) {
|
|
135
|
+
if (typeof value !== 'string') {
|
|
136
|
+
throw new Error(`owner action ${field} must be a non-blank string`);
|
|
137
|
+
}
|
|
138
|
+
if (value.trim().length === 0 || value.trim() !== value) {
|
|
139
|
+
throw new Error(`owner action ${field} must be a non-blank string without surrounding space`);
|
|
140
|
+
}
|
|
141
|
+
if (value.length > OWNER_ACTION_IDENTITY_MAX_LENGTH) {
|
|
142
|
+
throw new Error(`owner action ${field} must be at most ${OWNER_ACTION_IDENTITY_MAX_LENGTH} characters`);
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Same identity rules as OwnerActionEffectLedger: every field is a trusted
|
|
148
|
+
* host identity, never model text. Checked before any storage is touched.
|
|
149
|
+
*/
|
|
150
|
+
function requireOwnerActionContext(context) {
|
|
151
|
+
if (context === null || typeof context !== 'object') {
|
|
152
|
+
throw new Error('owner action context is required');
|
|
153
|
+
}
|
|
154
|
+
const workOrderAttemptId = context.workOrderAttemptId;
|
|
155
|
+
if (workOrderAttemptId !== undefined &&
|
|
156
|
+
(!Number.isSafeInteger(workOrderAttemptId) || workOrderAttemptId <= 0)) {
|
|
157
|
+
throw new Error('owner action workOrderAttemptId must be a positive integer when present');
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
ownerScope: requireOwnerActionIdentity(context.ownerScope, 'ownerScope'),
|
|
161
|
+
occurrenceKey: requireOwnerActionIdentity(context.occurrenceKey, 'occurrenceKey'),
|
|
162
|
+
modelRunId: requireOwnerActionIdentity(context.modelRunId, 'modelRunId'),
|
|
163
|
+
envelopeHash: requireOwnerActionIdentity(context.envelopeHash, 'envelopeHash'),
|
|
164
|
+
workOrderAttemptId: workOrderAttemptId ?? null,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
132
167
|
function assertEnum(value, allowed, field) {
|
|
133
168
|
if (!allowed.includes(value)) {
|
|
134
169
|
throw new Error(`${field} must be one of ${allowed.join('|')}, got: ${value}`);
|
|
@@ -189,6 +224,7 @@ function rowToRecord(row, now, timeZone) {
|
|
|
189
224
|
}, now, timeZone),
|
|
190
225
|
reviewStartedAt: row.review_started_at,
|
|
191
226
|
reviewAnchorEventId: row.review_anchor_event_id,
|
|
227
|
+
reviewAnchorSourceChannel: row.review_anchor_source_channel ?? null,
|
|
192
228
|
completionCriteria: row.completion_criteria ?? null,
|
|
193
229
|
resolutionKind: (row.resolution_kind ?? null),
|
|
194
230
|
createdAt: row.created_at,
|
|
@@ -199,10 +235,14 @@ class TaskLedger {
|
|
|
199
235
|
db;
|
|
200
236
|
now;
|
|
201
237
|
timeZone;
|
|
238
|
+
onOwnerTaskChangeCommitted;
|
|
239
|
+
pendingOwnerTaskGeneration = null;
|
|
240
|
+
ownerTaskNotificationScheduled = false;
|
|
202
241
|
constructor(db, options = {}) {
|
|
203
242
|
this.db = db;
|
|
204
243
|
this.now = options.now ?? Date.now;
|
|
205
244
|
this.timeZone = options.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
245
|
+
this.onOwnerTaskChangeCommitted = options.onOwnerTaskChangeCommitted;
|
|
206
246
|
// Validate the injected or boot-resolved zone once, before reads depend on it.
|
|
207
247
|
new Intl.DateTimeFormat('en-US', { timeZone: this.timeZone }).format(0);
|
|
208
248
|
this.runMigration();
|
|
@@ -270,6 +310,7 @@ class TaskLedger {
|
|
|
270
310
|
last_temporal_attempt_id INTEGER,
|
|
271
311
|
review_started_at INTEGER,
|
|
272
312
|
review_anchor_event_id TEXT,
|
|
313
|
+
review_anchor_source_channel TEXT,
|
|
273
314
|
completion_criteria TEXT,
|
|
274
315
|
resolution_kind TEXT,
|
|
275
316
|
created_at INTEGER NOT NULL,
|
|
@@ -337,6 +378,7 @@ class TaskLedger {
|
|
|
337
378
|
}
|
|
338
379
|
(0, operator_task_temporal_js_1.applyOperatorTaskTemporalMigration)(this.db);
|
|
339
380
|
(0, operator_task_external_lifecycle_js_1.applyOperatorTaskExternalLifecycleMigration)(this.db);
|
|
381
|
+
(0, operator_owner_action_candidates_js_1.applyOperatorOwnerActionCandidatesMigration)(this.db);
|
|
340
382
|
(0, operator_task_review_lifecycle_js_1.applyOperatorTaskReviewLifecycleMigration)(this.db);
|
|
341
383
|
(0, operator_task_classification_js_1.applyOperatorTaskClassificationMigration)(this.db);
|
|
342
384
|
// Old-predicate unique index (no terminal exclusion) -> swap in place.
|
|
@@ -596,6 +638,37 @@ class TaskLedger {
|
|
|
596
638
|
.digest('base64url')
|
|
597
639
|
.slice(0, 22);
|
|
598
640
|
}
|
|
641
|
+
notifyOwnerTaskCommit(previousGeneration) {
|
|
642
|
+
if (!this.onOwnerTaskChangeCommitted) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const generation = this.readGeneration();
|
|
646
|
+
if (generation === previousGeneration) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
this.pendingOwnerTaskGeneration = generation;
|
|
650
|
+
if (this.ownerTaskNotificationScheduled) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
this.ownerTaskNotificationScheduled = true;
|
|
654
|
+
queueMicrotask(() => this.flushOwnerTaskNotification());
|
|
655
|
+
}
|
|
656
|
+
flushOwnerTaskNotification() {
|
|
657
|
+
this.ownerTaskNotificationScheduled = false;
|
|
658
|
+
const expected = this.pendingOwnerTaskGeneration;
|
|
659
|
+
this.pendingOwnerTaskGeneration = null;
|
|
660
|
+
if (!expected || !this.onOwnerTaskChangeCommitted) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const current = this.readGeneration();
|
|
664
|
+
if (current !== expected) {
|
|
665
|
+
this.pendingOwnerTaskGeneration = current;
|
|
666
|
+
this.ownerTaskNotificationScheduled = true;
|
|
667
|
+
queueMicrotask(() => this.flushOwnerTaskNotification());
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
this.onOwnerTaskChangeCommitted(current);
|
|
671
|
+
}
|
|
599
672
|
/**
|
|
600
673
|
* Aggregate counts over the same owner predicate `listPage` filters by. Facets
|
|
601
674
|
* come from SQL GROUP BY (each partitions total); due buckets are computed in
|
|
@@ -679,7 +752,8 @@ class TaskLedger {
|
|
|
679
752
|
getExternalBinding(taskId) {
|
|
680
753
|
const row = this.db
|
|
681
754
|
.prepare(`SELECT id, revision, task_id, connector, source_type, external_source_id,
|
|
682
|
-
last_observation_seq, created_by_attempt_id
|
|
755
|
+
last_observation_seq, created_by_attempt_id, created_by_run_id,
|
|
756
|
+
created_by_owner_scope, created_at
|
|
683
757
|
FROM operator_external_task_bindings
|
|
684
758
|
WHERE task_id = ? AND active = 1`)
|
|
685
759
|
.get(taskId);
|
|
@@ -818,6 +892,218 @@ class TaskLedger {
|
|
|
818
892
|
if (all.length === 0) {
|
|
819
893
|
return { disposition: 'none' };
|
|
820
894
|
}
|
|
895
|
+
// Receipts are global by candidate id, so a decision settled by an owner
|
|
896
|
+
// run is seen here exactly like one settled by this attempt.
|
|
897
|
+
return this.candidateReceiptDisposition(all);
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Resolve one candidate decision's authority (Task B2).
|
|
901
|
+
*
|
|
902
|
+
* A plain attempt id is the legacy Board wrapper. A discriminated source is
|
|
903
|
+
* either that same Board path or a verified owner run whose host-attested
|
|
904
|
+
* snapshot must match the caller's scope, run and envelope exactly. Both
|
|
905
|
+
* return the SAME decision inputs; the receipt records which one settled it.
|
|
906
|
+
*/
|
|
907
|
+
resolveExternalCandidateAuthority(source, kind, origin) {
|
|
908
|
+
const resolved = typeof source === 'number' ? { kind: 'board', attemptId: source } : source;
|
|
909
|
+
const label = kind === 'binding' ? 'external binding' : 'external lifecycle';
|
|
910
|
+
if (resolved.kind === 'board') {
|
|
911
|
+
const attemptId = resolved.attemptId;
|
|
912
|
+
if (origin.workOrderAttemptId !== attemptId) {
|
|
913
|
+
throw new Error(`${label} decision requires trusted attempt origin ${attemptId}`);
|
|
914
|
+
}
|
|
915
|
+
return {
|
|
916
|
+
source: resolved,
|
|
917
|
+
receiptOrigin: {
|
|
918
|
+
attemptId,
|
|
919
|
+
runId: origin.runId ?? null,
|
|
920
|
+
ownerScope: null,
|
|
921
|
+
envelopeHash: null,
|
|
922
|
+
},
|
|
923
|
+
load: (candidateId) => {
|
|
924
|
+
const attempt = this.getWorkOrderById(attemptId);
|
|
925
|
+
return {
|
|
926
|
+
candidate: this.loadBoardCandidate(attemptId, candidateId, kind),
|
|
927
|
+
snapshotAt: attempt?.createdAt ?? 0,
|
|
928
|
+
};
|
|
929
|
+
},
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
const context = requireOwnerActionContext(resolved.context);
|
|
933
|
+
if (origin.runId !== context.modelRunId) {
|
|
934
|
+
throw new Error(`${label} decision requires trusted owner run origin ${context.modelRunId}`);
|
|
935
|
+
}
|
|
936
|
+
if ((origin.workOrderAttemptId ?? null) !== context.workOrderAttemptId) {
|
|
937
|
+
throw new Error(`${label} decision attempt ${String(origin.workOrderAttemptId)} does not match the attesting owner run`);
|
|
938
|
+
}
|
|
939
|
+
if (context.workOrderAttemptId !== null) {
|
|
940
|
+
this.assertWorkOrderAttemptActive(label, context.workOrderAttemptId);
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
source: resolved,
|
|
944
|
+
receiptOrigin: {
|
|
945
|
+
attemptId: context.workOrderAttemptId,
|
|
946
|
+
runId: context.modelRunId,
|
|
947
|
+
ownerScope: context.ownerScope,
|
|
948
|
+
envelopeHash: context.envelopeHash,
|
|
949
|
+
},
|
|
950
|
+
load: (candidateId) => {
|
|
951
|
+
const attested = this.loadOwnerAttestedCandidate(context, candidateId, kind);
|
|
952
|
+
return { candidate: attested.candidate, snapshotAt: attested.attestedAt };
|
|
953
|
+
},
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
/** Same contract as loadBoardCandidate, for either authority source. */
|
|
957
|
+
loadExternalCandidate(source, candidateId, kind) {
|
|
958
|
+
const resolved = typeof source === 'number' ? { kind: 'board', attemptId: source } : source;
|
|
959
|
+
return resolved.kind === 'board'
|
|
960
|
+
? this.loadBoardCandidate(resolved.attemptId, candidateId, kind)
|
|
961
|
+
: this.loadOwnerActionCandidate(resolved.context, candidateId, kind);
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Persist host-built candidate snapshots under a verified owner run.
|
|
965
|
+
*
|
|
966
|
+
* Every snapshot is re-validated as host-built (exact shape, Kagemusha
|
|
967
|
+
* mapping, fixed evidence summary, content-derived id) before it is stored;
|
|
968
|
+
* model-authored bytes never enter this table. Rows are immutable. The same
|
|
969
|
+
* run attesting the same candidate again with identical bytes is a no-op;
|
|
970
|
+
* different bytes under the same id are a conflict, not a second row.
|
|
971
|
+
*/
|
|
972
|
+
attestOwnerActionCandidates(context, candidates) {
|
|
973
|
+
const verified = requireOwnerActionContext(context);
|
|
974
|
+
const prepared = [
|
|
975
|
+
...candidates.bindingCandidates.map((candidate) => (0, external_lifecycle_candidates_js_1.parseHostBuiltExternalLifecycleCandidate)('binding', candidate)),
|
|
976
|
+
...candidates.lifecycleCandidates.map((candidate) => (0, external_lifecycle_candidates_js_1.parseHostBuiltExternalLifecycleCandidate)('lifecycle', candidate)),
|
|
977
|
+
].map((candidate) => {
|
|
978
|
+
const json = (0, external_lifecycle_candidates_js_1.canonicalExternalLifecycleCandidateJson)(candidate);
|
|
979
|
+
return { candidate, json, sha256: (0, node_crypto_1.createHash)('sha256').update(json).digest('hex') };
|
|
980
|
+
});
|
|
981
|
+
let attested = 0;
|
|
982
|
+
let alreadyAttested = 0;
|
|
983
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
984
|
+
try {
|
|
985
|
+
for (const entry of prepared) {
|
|
986
|
+
const existing = this.db
|
|
987
|
+
.prepare(`SELECT owner_scope, envelope_hash, occurrence_key, workorder_attempt_id,
|
|
988
|
+
candidate_kind, candidate_sha256
|
|
989
|
+
FROM ${operator_owner_action_candidates_js_1.OWNER_ACTION_CANDIDATES_TABLE}
|
|
990
|
+
WHERE model_run_id = ? AND candidate_id = ?`)
|
|
991
|
+
.get(verified.modelRunId, entry.candidate.candidateId);
|
|
992
|
+
if (existing) {
|
|
993
|
+
if (existing.owner_scope !== verified.ownerScope ||
|
|
994
|
+
existing.envelope_hash !== verified.envelopeHash ||
|
|
995
|
+
existing.occurrence_key !== verified.occurrenceKey ||
|
|
996
|
+
existing.workorder_attempt_id !== verified.workOrderAttemptId ||
|
|
997
|
+
existing.candidate_kind !== entry.candidate.kind ||
|
|
998
|
+
existing.candidate_sha256 !== entry.sha256) {
|
|
999
|
+
throw new Error(`owner action candidate ${entry.candidate.candidateId} attestation conflict: run ${verified.modelRunId} already holds a snapshot that differs`);
|
|
1000
|
+
}
|
|
1001
|
+
alreadyAttested += 1;
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
const task = this.getRowById(entry.candidate.taskId);
|
|
1005
|
+
if (!task || task.kind !== 'owner') {
|
|
1006
|
+
throw new Error(`owner action candidate ${entry.candidate.candidateId} references no owner task`);
|
|
1007
|
+
}
|
|
1008
|
+
this.db
|
|
1009
|
+
.prepare(`INSERT INTO ${operator_owner_action_candidates_js_1.OWNER_ACTION_CANDIDATES_TABLE}
|
|
1010
|
+
(model_run_id, candidate_id, owner_scope, envelope_hash, occurrence_key,
|
|
1011
|
+
workorder_attempt_id, candidate_kind, task_id, event_id, candidate_json,
|
|
1012
|
+
candidate_sha256, created_at)
|
|
1013
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1014
|
+
.run(verified.modelRunId, entry.candidate.candidateId, verified.ownerScope, verified.envelopeHash, verified.occurrenceKey, verified.workOrderAttemptId, entry.candidate.kind, entry.candidate.taskId, entry.candidate.eventId, entry.json, entry.sha256, this.now());
|
|
1015
|
+
attested += 1;
|
|
1016
|
+
}
|
|
1017
|
+
this.db.exec('COMMIT');
|
|
1018
|
+
}
|
|
1019
|
+
catch (error) {
|
|
1020
|
+
this.db.exec('ROLLBACK');
|
|
1021
|
+
throw error;
|
|
1022
|
+
}
|
|
1023
|
+
return { attested, alreadyAttested };
|
|
1024
|
+
}
|
|
1025
|
+
/** Bounded, intent-free listing of what one owner run attested. */
|
|
1026
|
+
listOwnerActionCandidates(context) {
|
|
1027
|
+
const verified = requireOwnerActionContext(context);
|
|
1028
|
+
return this.db
|
|
1029
|
+
.prepare(`SELECT candidate_id, candidate_kind, task_id, event_id
|
|
1030
|
+
FROM ${operator_owner_action_candidates_js_1.OWNER_ACTION_CANDIDATES_TABLE}
|
|
1031
|
+
WHERE model_run_id = ? AND owner_scope = ? AND envelope_hash = ?
|
|
1032
|
+
AND occurrence_key = ? AND workorder_attempt_id IS ?
|
|
1033
|
+
ORDER BY created_at ASC, candidate_kind ASC, candidate_id ASC`)
|
|
1034
|
+
.all(verified.modelRunId, verified.ownerScope, verified.envelopeHash, verified.occurrenceKey, verified.workOrderAttemptId).map((row) => ({
|
|
1035
|
+
candidateId: row.candidate_id,
|
|
1036
|
+
kind: row.candidate_kind,
|
|
1037
|
+
taskId: row.task_id,
|
|
1038
|
+
eventId: row.event_id,
|
|
1039
|
+
}));
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Receipt view of one owner run's attested candidates, in the same shape as
|
|
1043
|
+
* inspectBoardCandidateAttempt so recovery can treat both sources alike.
|
|
1044
|
+
*/
|
|
1045
|
+
inspectOwnerActionCandidateRun(context) {
|
|
1046
|
+
const all = this.listOwnerActionCandidates(context);
|
|
1047
|
+
if (all.length === 0) {
|
|
1048
|
+
return { disposition: 'none' };
|
|
1049
|
+
}
|
|
1050
|
+
return this.candidateReceiptDisposition(all);
|
|
1051
|
+
}
|
|
1052
|
+
loadOwnerActionCandidate(context, candidateId, kind) {
|
|
1053
|
+
return this.loadOwnerAttestedCandidate(requireOwnerActionContext(context), candidateId, kind)
|
|
1054
|
+
.candidate;
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Recover a candidate only from the attestation the exact owner run made.
|
|
1058
|
+
* Stored bytes are revalidated: sha256 over the canonical JSON, exact shape,
|
|
1059
|
+
* and the content-derived candidate id must all still agree.
|
|
1060
|
+
*/
|
|
1061
|
+
loadOwnerAttestedCandidate(context, candidateId, kind) {
|
|
1062
|
+
const row = this.db
|
|
1063
|
+
.prepare(`SELECT owner_scope, envelope_hash, occurrence_key, workorder_attempt_id,
|
|
1064
|
+
candidate_kind, task_id, event_id, candidate_json, candidate_sha256, created_at
|
|
1065
|
+
FROM ${operator_owner_action_candidates_js_1.OWNER_ACTION_CANDIDATES_TABLE}
|
|
1066
|
+
WHERE model_run_id = ? AND candidate_id = ?`)
|
|
1067
|
+
.get(context.modelRunId, candidateId);
|
|
1068
|
+
if (!row) {
|
|
1069
|
+
throw new Error(`external lifecycle ${kind} candidate ${candidateId} is not attested for owner run ${context.modelRunId}`);
|
|
1070
|
+
}
|
|
1071
|
+
if (row.owner_scope !== context.ownerScope) {
|
|
1072
|
+
throw new Error(`external lifecycle ${kind} candidate ${candidateId} was attested under a different owner scope`);
|
|
1073
|
+
}
|
|
1074
|
+
if (row.envelope_hash !== context.envelopeHash) {
|
|
1075
|
+
throw new Error(`external lifecycle ${kind} candidate ${candidateId} was attested under a different envelope`);
|
|
1076
|
+
}
|
|
1077
|
+
if (row.occurrence_key !== context.occurrenceKey) {
|
|
1078
|
+
throw new Error(`external lifecycle ${kind} candidate ${candidateId} was attested under a different occurrence`);
|
|
1079
|
+
}
|
|
1080
|
+
if (row.workorder_attempt_id !== context.workOrderAttemptId) {
|
|
1081
|
+
throw new Error(`external lifecycle ${kind} candidate ${candidateId} was attested under a different workorder attempt`);
|
|
1082
|
+
}
|
|
1083
|
+
if (row.candidate_kind !== kind) {
|
|
1084
|
+
throw new Error(`external lifecycle candidate ${candidateId} has the wrong kind (attested as ${row.candidate_kind})`);
|
|
1085
|
+
}
|
|
1086
|
+
const digest = (0, node_crypto_1.createHash)('sha256').update(row.candidate_json).digest('hex');
|
|
1087
|
+
if (digest !== row.candidate_sha256) {
|
|
1088
|
+
throw new Error(`external lifecycle ${kind} candidate ${candidateId} failed attestation integrity (sha256 mismatch)`);
|
|
1089
|
+
}
|
|
1090
|
+
let parsed;
|
|
1091
|
+
try {
|
|
1092
|
+
parsed = JSON.parse(row.candidate_json);
|
|
1093
|
+
}
|
|
1094
|
+
catch {
|
|
1095
|
+
throw new Error(`external lifecycle ${kind} candidate ${candidateId} failed attestation integrity (unreadable)`);
|
|
1096
|
+
}
|
|
1097
|
+
const candidate = (0, external_lifecycle_candidates_js_1.parseHostBuiltExternalLifecycleCandidate)(kind, parsed);
|
|
1098
|
+
if (candidate.candidateId !== candidateId ||
|
|
1099
|
+
candidate.taskId !== row.task_id ||
|
|
1100
|
+
candidate.eventId !== row.event_id ||
|
|
1101
|
+
(0, external_lifecycle_candidates_js_1.canonicalExternalLifecycleCandidateJson)(candidate) !== row.candidate_json) {
|
|
1102
|
+
throw new Error(`external lifecycle ${kind} candidate ${candidateId} failed attestation integrity (identity mismatch)`);
|
|
1103
|
+
}
|
|
1104
|
+
return { candidate, attestedAt: row.created_at };
|
|
1105
|
+
}
|
|
1106
|
+
candidateReceiptDisposition(all) {
|
|
821
1107
|
const outcomes = [];
|
|
822
1108
|
const missingCandidateIds = [];
|
|
823
1109
|
for (const candidate of all) {
|
|
@@ -839,15 +1125,14 @@ class TaskLedger {
|
|
|
839
1125
|
}
|
|
840
1126
|
return { disposition: 'partial', missingCandidateIds };
|
|
841
1127
|
}
|
|
842
|
-
applyExternalBindingDecision(
|
|
1128
|
+
applyExternalBindingDecision(source, input, origin) {
|
|
843
1129
|
(0, external_lifecycle_candidates_js_1.validateExternalLifecycleDecision)('binding', input);
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
}
|
|
1130
|
+
const authority = this.resolveExternalCandidateAuthority(source, 'binding', origin);
|
|
1131
|
+
const receiptOrigin = authority.receiptOrigin;
|
|
847
1132
|
let receipt = null;
|
|
848
1133
|
this.db.exec('BEGIN IMMEDIATE');
|
|
849
1134
|
try {
|
|
850
|
-
const candidate =
|
|
1135
|
+
const { candidate } = authority.load(input.candidate_id);
|
|
851
1136
|
if (candidate.kind !== 'binding') {
|
|
852
1137
|
throw new Error(`external lifecycle candidate ${input.candidate_id} has the wrong kind`);
|
|
853
1138
|
}
|
|
@@ -888,16 +1173,19 @@ class TaskLedger {
|
|
|
888
1173
|
const inserted = this.db
|
|
889
1174
|
.prepare(`INSERT INTO operator_external_task_bindings
|
|
890
1175
|
(task_id, connector, source_type, external_source_id, last_observation_seq,
|
|
891
|
-
created_by_attempt_id,
|
|
892
|
-
|
|
893
|
-
|
|
1176
|
+
created_by_attempt_id, created_by_run_id, created_by_owner_scope,
|
|
1177
|
+
created_by_envelope_hash, active, created_at, updated_at)
|
|
1178
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)`)
|
|
1179
|
+
.run(candidate.taskId, candidate.connector, candidate.sourceType, candidate.externalSourceId, candidate.operatorObservationSeq, receiptOrigin.attemptId, receiptOrigin.runId, receiptOrigin.ownerScope, receiptOrigin.envelopeHash, now, now);
|
|
894
1180
|
bindingId = Number(inserted.lastInsertRowid);
|
|
895
1181
|
}
|
|
896
|
-
this.insertBindingReceipt(candidate,
|
|
1182
|
+
this.insertBindingReceipt(candidate, receiptOrigin, input, outcome, bindingId, origin);
|
|
897
1183
|
receipt = {
|
|
898
1184
|
kind: 'binding',
|
|
899
1185
|
candidateId: candidate.candidateId,
|
|
900
|
-
workOrderAttemptId: attemptId,
|
|
1186
|
+
workOrderAttemptId: receiptOrigin.attemptId,
|
|
1187
|
+
originRunId: receiptOrigin.runId,
|
|
1188
|
+
originOwnerScope: receiptOrigin.ownerScope,
|
|
901
1189
|
taskId: candidate.taskId,
|
|
902
1190
|
outcome,
|
|
903
1191
|
reason: input.reason,
|
|
@@ -912,15 +1200,15 @@ class TaskLedger {
|
|
|
912
1200
|
}
|
|
913
1201
|
return receipt;
|
|
914
1202
|
}
|
|
915
|
-
applyExternalLifecycleDecision(
|
|
1203
|
+
applyExternalLifecycleDecision(source, input, origin) {
|
|
916
1204
|
(0, external_lifecycle_candidates_js_1.validateExternalLifecycleDecision)('lifecycle', input);
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
}
|
|
1205
|
+
const authority = this.resolveExternalCandidateAuthority(source, 'lifecycle', origin);
|
|
1206
|
+
const receiptOrigin = authority.receiptOrigin;
|
|
920
1207
|
let receipt = null;
|
|
1208
|
+
const previousGeneration = this.onOwnerTaskChangeCommitted ? this.readGeneration() : null;
|
|
921
1209
|
this.db.exec('BEGIN IMMEDIATE');
|
|
922
1210
|
try {
|
|
923
|
-
const candidate =
|
|
1211
|
+
const { candidate, snapshotAt } = authority.load(input.candidate_id);
|
|
924
1212
|
if (candidate.kind !== 'lifecycle') {
|
|
925
1213
|
throw new Error(`external lifecycle candidate ${input.candidate_id} has the wrong kind`);
|
|
926
1214
|
}
|
|
@@ -944,12 +1232,19 @@ class TaskLedger {
|
|
|
944
1232
|
if (!task || task.kind !== 'owner' || !binding) {
|
|
945
1233
|
throw new Error(`external lifecycle candidate ${candidate.candidateId} has no active binding`);
|
|
946
1234
|
}
|
|
1235
|
+
// A binding that came into being after this candidate's snapshot cannot
|
|
1236
|
+
// be the binding the snapshot described. Board attempts keep the exact
|
|
1237
|
+
// legacy ordering rule; when either side lacks an attempt id (owner-run
|
|
1238
|
+
// decisions), the ledger clock decides.
|
|
1239
|
+
const bindingPostdatesSnapshot = authority.source.kind === 'board' && binding.createdByAttemptId !== null
|
|
1240
|
+
? binding.createdByAttemptId >= authority.source.attemptId
|
|
1241
|
+
: binding.createdAt > snapshotAt;
|
|
947
1242
|
const superseded = binding.id !== candidate.bindingId ||
|
|
948
1243
|
binding.revision !== candidate.bindingRevision ||
|
|
949
1244
|
binding.connector !== candidate.connector ||
|
|
950
1245
|
binding.sourceType !== candidate.sourceType ||
|
|
951
1246
|
binding.externalSourceId !== candidate.externalSourceId ||
|
|
952
|
-
|
|
1247
|
+
bindingPostdatesSnapshot ||
|
|
953
1248
|
task.revision !== candidate.taskRevision ||
|
|
954
1249
|
candidate.operatorObservationSeq <= binding.lastObservationSeq;
|
|
955
1250
|
const taskRevisionBefore = task.revision;
|
|
@@ -984,11 +1279,13 @@ class TaskLedger {
|
|
|
984
1279
|
throw new Error(`external lifecycle candidate ${candidate.candidateId} binding watermark changed`);
|
|
985
1280
|
}
|
|
986
1281
|
}
|
|
987
|
-
this.insertLifecycleReceipt(candidate,
|
|
1282
|
+
this.insertLifecycleReceipt(candidate, receiptOrigin, input, outcome, taskRevisionBefore, taskRevisionAfter, origin);
|
|
988
1283
|
receipt = {
|
|
989
1284
|
kind: 'lifecycle',
|
|
990
1285
|
candidateId: candidate.candidateId,
|
|
991
|
-
workOrderAttemptId: attemptId,
|
|
1286
|
+
workOrderAttemptId: receiptOrigin.attemptId,
|
|
1287
|
+
originRunId: receiptOrigin.runId,
|
|
1288
|
+
originOwnerScope: receiptOrigin.ownerScope,
|
|
992
1289
|
taskId: candidate.taskId,
|
|
993
1290
|
outcome,
|
|
994
1291
|
reason: input.reason,
|
|
@@ -1002,16 +1299,18 @@ class TaskLedger {
|
|
|
1002
1299
|
this.db.exec('ROLLBACK');
|
|
1003
1300
|
throw error;
|
|
1004
1301
|
}
|
|
1302
|
+
this.notifyOwnerTaskCommit(previousGeneration);
|
|
1005
1303
|
return receipt;
|
|
1006
1304
|
}
|
|
1007
1305
|
getExternalCandidateReceipt(candidateId) {
|
|
1008
1306
|
const binding = this.db
|
|
1009
|
-
.prepare(`SELECT candidate_id, workorder_attempt_id,
|
|
1307
|
+
.prepare(`SELECT candidate_id, workorder_attempt_id, origin_run_id, origin_owner_scope, task_id,
|
|
1308
|
+
outcome, reason, binding_id
|
|
1010
1309
|
FROM operator_external_binding_receipts WHERE candidate_id = ?`)
|
|
1011
1310
|
.get(candidateId);
|
|
1012
1311
|
const lifecycle = this.db
|
|
1013
|
-
.prepare(`SELECT candidate_id, workorder_attempt_id,
|
|
1014
|
-
task_revision_before, task_revision_after
|
|
1312
|
+
.prepare(`SELECT candidate_id, workorder_attempt_id, origin_run_id, origin_owner_scope, task_id,
|
|
1313
|
+
outcome, reason, task_revision_before, task_revision_after
|
|
1015
1314
|
FROM operator_external_lifecycle_receipts WHERE candidate_id = ?`)
|
|
1016
1315
|
.get(candidateId);
|
|
1017
1316
|
if (binding && lifecycle) {
|
|
@@ -1035,6 +1334,8 @@ class TaskLedger {
|
|
|
1035
1334
|
kind: 'binding',
|
|
1036
1335
|
candidateId: binding.candidate_id,
|
|
1037
1336
|
workOrderAttemptId: binding.workorder_attempt_id,
|
|
1337
|
+
originRunId: binding.origin_run_id,
|
|
1338
|
+
originOwnerScope: binding.origin_owner_scope,
|
|
1038
1339
|
taskId: binding.task_id,
|
|
1039
1340
|
outcome: binding.outcome,
|
|
1040
1341
|
reason: binding.reason,
|
|
@@ -1046,6 +1347,8 @@ class TaskLedger {
|
|
|
1046
1347
|
kind: 'lifecycle',
|
|
1047
1348
|
candidateId: lifecycle.candidate_id,
|
|
1048
1349
|
workOrderAttemptId: lifecycle.workorder_attempt_id,
|
|
1350
|
+
originRunId: lifecycle.origin_run_id,
|
|
1351
|
+
originOwnerScope: lifecycle.origin_owner_scope,
|
|
1049
1352
|
taskId: lifecycle.task_id,
|
|
1050
1353
|
outcome: lifecycle.outcome,
|
|
1051
1354
|
reason: lifecycle.reason,
|
|
@@ -1064,6 +1367,9 @@ class TaskLedger {
|
|
|
1064
1367
|
externalSourceId: row.external_source_id,
|
|
1065
1368
|
lastObservationSeq: row.last_observation_seq,
|
|
1066
1369
|
createdByAttemptId: row.created_by_attempt_id,
|
|
1370
|
+
createdByRunId: row.created_by_run_id,
|
|
1371
|
+
createdByOwnerScope: row.created_by_owner_scope,
|
|
1372
|
+
createdAt: row.created_at,
|
|
1067
1373
|
};
|
|
1068
1374
|
}
|
|
1069
1375
|
assertBindingReplayMatches(receipt, candidate, input) {
|
|
@@ -1077,15 +1383,16 @@ class TaskLedger {
|
|
|
1077
1383
|
throw new Error(`external binding receipt ${candidate.candidateId} conflicts with replay decision`);
|
|
1078
1384
|
}
|
|
1079
1385
|
}
|
|
1080
|
-
insertBindingReceipt(candidate,
|
|
1386
|
+
insertBindingReceipt(candidate, receiptOrigin, input, outcome, bindingId, origin) {
|
|
1081
1387
|
this.db
|
|
1082
1388
|
.prepare(`INSERT INTO operator_external_binding_receipts
|
|
1083
1389
|
(candidate_id, decision, workorder_attempt_id, task_id, event_id, connector, source_type,
|
|
1084
1390
|
external_source_id, channel_partition, content_sha256, source_timestamp_ms,
|
|
1085
1391
|
operator_ingest_seq, operator_observation_seq, task_revision, outcome, reason, binding_id,
|
|
1086
|
-
origin_run_id, origin_cause_event_ids,
|
|
1087
|
-
|
|
1088
|
-
|
|
1392
|
+
origin_run_id, origin_cause_event_ids, origin_owner_scope, origin_envelope_hash,
|
|
1393
|
+
created_at)
|
|
1394
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1395
|
+
.run(candidate.candidateId, input.decision, receiptOrigin.attemptId, candidate.taskId, candidate.eventId, candidate.connector, candidate.sourceType, candidate.externalSourceId, candidate.channelPartition, candidate.contentSha256, candidate.sourceTimestampMs, candidate.operatorIngestSeq, candidate.operatorObservationSeq, candidate.taskRevision, outcome, input.reason, bindingId ?? null, receiptOrigin.runId, JSON.stringify(origin.causeEventIds ?? []), receiptOrigin.ownerScope, receiptOrigin.envelopeHash, this.now());
|
|
1089
1396
|
}
|
|
1090
1397
|
assertLifecycleReplayMatches(receipt, candidate, input) {
|
|
1091
1398
|
const stored = this.db
|
|
@@ -1098,16 +1405,16 @@ class TaskLedger {
|
|
|
1098
1405
|
throw new Error(`external lifecycle receipt ${candidate.candidateId} conflicts with replay decision`);
|
|
1099
1406
|
}
|
|
1100
1407
|
}
|
|
1101
|
-
insertLifecycleReceipt(candidate,
|
|
1408
|
+
insertLifecycleReceipt(candidate, receiptOrigin, input, outcome, taskRevisionBefore, taskRevisionAfter, origin) {
|
|
1102
1409
|
this.db
|
|
1103
1410
|
.prepare(`INSERT INTO operator_external_lifecycle_receipts
|
|
1104
1411
|
(candidate_id, decision, workorder_attempt_id, task_id, event_id, connector, source_type,
|
|
1105
1412
|
external_source_id, channel_partition, content_sha256, source_timestamp_ms,
|
|
1106
1413
|
operator_ingest_seq, operator_observation_seq, binding_id, binding_revision,
|
|
1107
1414
|
task_revision_before, task_revision_after, outcome, reason, origin_run_id,
|
|
1108
|
-
origin_cause_event_ids, created_at)
|
|
1109
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1110
|
-
.run(candidate.candidateId, input.decision, attemptId, candidate.taskId, candidate.eventId, candidate.connector, candidate.sourceType, candidate.externalSourceId, candidate.channelPartition, candidate.contentSha256, candidate.sourceTimestampMs, candidate.operatorIngestSeq, candidate.operatorObservationSeq, candidate.bindingId, candidate.bindingRevision, taskRevisionBefore, taskRevisionAfter, outcome, input.reason,
|
|
1415
|
+
origin_cause_event_ids, origin_owner_scope, origin_envelope_hash, created_at)
|
|
1416
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1417
|
+
.run(candidate.candidateId, input.decision, receiptOrigin.attemptId, candidate.taskId, candidate.eventId, candidate.connector, candidate.sourceType, candidate.externalSourceId, candidate.channelPartition, candidate.contentSha256, candidate.sourceTimestampMs, candidate.operatorIngestSeq, candidate.operatorObservationSeq, candidate.bindingId, candidate.bindingRevision, taskRevisionBefore, taskRevisionAfter, outcome, input.reason, receiptOrigin.runId, JSON.stringify(origin.causeEventIds ?? []), receiptOrigin.ownerScope, receiptOrigin.envelopeHash, this.now());
|
|
1111
1418
|
}
|
|
1112
1419
|
/**
|
|
1113
1420
|
* Create a task. Idempotent under at-least-once delivery: a duplicate
|
|
@@ -1194,8 +1501,8 @@ class TaskLedger {
|
|
|
1194
1501
|
}
|
|
1195
1502
|
}
|
|
1196
1503
|
let createdId = 0;
|
|
1197
|
-
this.
|
|
1198
|
-
|
|
1504
|
+
const previousGeneration = this.onOwnerTaskChangeCommitted ? this.readGeneration() : null;
|
|
1505
|
+
const write = this.db.transaction(() => {
|
|
1199
1506
|
const result = this.db
|
|
1200
1507
|
.prepare(`INSERT INTO operator_tasks
|
|
1201
1508
|
(title, status, priority, assignee, deadline, due_at, deadline_offset_minutes,
|
|
@@ -1247,12 +1554,9 @@ class TaskLedger {
|
|
|
1247
1554
|
},
|
|
1248
1555
|
atMs: now,
|
|
1249
1556
|
});
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
this.db.exec('ROLLBACK');
|
|
1254
|
-
throw error;
|
|
1255
|
-
}
|
|
1557
|
+
}, 'immediate');
|
|
1558
|
+
write();
|
|
1559
|
+
this.notifyOwnerTaskCommit(previousGeneration);
|
|
1256
1560
|
const created = this.getById(createdId);
|
|
1257
1561
|
if (!created)
|
|
1258
1562
|
throw new Error('task_create: inserted row could not be read back');
|
|
@@ -1294,16 +1598,10 @@ class TaskLedger {
|
|
|
1294
1598
|
}
|
|
1295
1599
|
update(id, patch, origin = {}) {
|
|
1296
1600
|
this.validateTaskUpdatePatch(patch);
|
|
1297
|
-
this.
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
return task;
|
|
1302
|
-
}
|
|
1303
|
-
catch (error) {
|
|
1304
|
-
this.db.exec('ROLLBACK');
|
|
1305
|
-
throw error;
|
|
1306
|
-
}
|
|
1601
|
+
const previousGeneration = this.onOwnerTaskChangeCommitted ? this.readGeneration() : null;
|
|
1602
|
+
const updated = this.db.transaction(() => this.transitionTaskInTransaction(id, patch, origin), 'immediate')();
|
|
1603
|
+
this.notifyOwnerTaskCommit(previousGeneration);
|
|
1604
|
+
return updated;
|
|
1307
1605
|
}
|
|
1308
1606
|
/**
|
|
1309
1607
|
* Recorrect an existing owner row with a NAMED disposition.
|
|
@@ -1314,9 +1612,12 @@ class TaskLedger {
|
|
|
1314
1612
|
* owner rows and could only be "closed" as if they had been done.
|
|
1315
1613
|
*
|
|
1316
1614
|
* Every path requires the exact current revision, a non-empty bounded reason
|
|
1317
|
-
* preserved as `latest_event`, an OWNER row, and the same
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
1615
|
+
* preserved as `latest_event`, an OWNER row, and the same workorder-attempt
|
|
1616
|
+
* currency and causal-provenance checks as task_update (TG-03/TG-04/TG-06) -
|
|
1617
|
+
* a terminal attempt of any kind cannot mutate a row, and an owner-event
|
|
1618
|
+
* decision cannot rest on a fabricated cause. Scheduler hint pages and the
|
|
1619
|
+
* causal batch's channel select what the host READS; they do not narrow the
|
|
1620
|
+
* authenticated owner's decisions. The whole transition, its
|
|
1320
1621
|
* `resolution_kind` and its effect receipt commit as ONE transaction.
|
|
1321
1622
|
*/
|
|
1322
1623
|
reclassify(id, input, origin = {}) {
|
|
@@ -1334,6 +1635,8 @@ class TaskLedger {
|
|
|
1334
1635
|
if (!Number.isSafeInteger(input.expected_revision) || input.expected_revision < 0) {
|
|
1335
1636
|
throw new Error('task_reclassify: expected_revision must be a non-negative integer');
|
|
1336
1637
|
}
|
|
1638
|
+
const previousGeneration = this.onOwnerTaskChangeCommitted ? this.readGeneration() : null;
|
|
1639
|
+
let record;
|
|
1337
1640
|
this.db.exec('BEGIN IMMEDIATE');
|
|
1338
1641
|
try {
|
|
1339
1642
|
const existing = this.db.prepare('SELECT * FROM operator_tasks WHERE id = ?').get(id);
|
|
@@ -1345,54 +1648,67 @@ class TaskLedger {
|
|
|
1345
1648
|
if (input.expected_revision !== existing.revision) {
|
|
1346
1649
|
throw new Error(`task_reclassify: expected revision ${input.expected_revision}, current ${existing.revision}`);
|
|
1347
1650
|
}
|
|
1348
|
-
this.assertTaskReclassificationAuthorized(
|
|
1651
|
+
this.assertTaskReclassificationAuthorized(origin);
|
|
1349
1652
|
const patch = this.buildReclassifyPatch(existing, disposition, reason);
|
|
1350
1653
|
// Same primitive as task_update: candidate guard, board-attempt liveness,
|
|
1351
1654
|
// revision-checked UPDATE, effect receipt, temporal generation supersession.
|
|
1352
|
-
|
|
1655
|
+
record = this.transitionTaskInTransaction(id, patch, origin);
|
|
1353
1656
|
this.db.exec('COMMIT');
|
|
1354
|
-
return record;
|
|
1355
1657
|
}
|
|
1356
1658
|
catch (error) {
|
|
1357
1659
|
this.db.exec('ROLLBACK');
|
|
1358
1660
|
throw error;
|
|
1359
1661
|
}
|
|
1662
|
+
this.notifyOwnerTaskCommit(previousGeneration);
|
|
1663
|
+
return record;
|
|
1360
1664
|
}
|
|
1361
|
-
assertTaskReclassificationAuthorized(
|
|
1665
|
+
assertTaskReclassificationAuthorized(origin) {
|
|
1362
1666
|
if (origin.workOrderAttemptId !== undefined) {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
(0, workorder_publishers_js_1.validateWorkOrderPayload)('board', payload);
|
|
1369
|
-
const candidates = Array.isArray(payload.reclassificationCandidates)
|
|
1370
|
-
? payload.reclassificationCandidates
|
|
1371
|
-
: [];
|
|
1372
|
-
const candidate = candidates.find((item) => item.taskId === existing.id);
|
|
1373
|
-
if (!candidate || candidate.taskRevision !== existing.revision) {
|
|
1374
|
-
throw new Error(`task_reclassify: task ${existing.id} is outside this host-issued Board candidate set`);
|
|
1375
|
-
}
|
|
1667
|
+
// A carried attempt must be CURRENT. Its kind is scheduling metadata: a
|
|
1668
|
+
// wiki or temporal run that discovers a record-shaped owner row may
|
|
1669
|
+
// reclassify it under the same CAS rules as a Board run. Its candidate
|
|
1670
|
+
// page is a bounded-read hint, so a row outside the page is not refused.
|
|
1671
|
+
this.assertWorkOrderAttemptActive('task_reclassify', origin.workOrderAttemptId);
|
|
1376
1672
|
}
|
|
1377
1673
|
if (origin.reclassificationCauseBound) {
|
|
1378
1674
|
const eventIds = (origin.causeEventIds ?? []).filter(effects_js_1.isUsableCause);
|
|
1379
1675
|
if (eventIds.length === 0) {
|
|
1380
1676
|
throw new Error('task_reclassify: owner-event reclassification requires causal events');
|
|
1381
1677
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
WHERE event.value IN (${placeholders})`)
|
|
1389
|
-
.all(...eventIds);
|
|
1390
|
-
if (existing.source_channel === null ||
|
|
1391
|
-
!channels.some((row) => row.channel_key === existing.source_channel)) {
|
|
1392
|
-
throw new Error(`task_reclassify: task ${existing.id} is outside this owner-event source channel`);
|
|
1678
|
+
// Provenance: every cause must be an event a durable inbox batch actually
|
|
1679
|
+
// carried. The batch's channel is not compared with the task's source -
|
|
1680
|
+
// cross-source relevance is MAMA's judgment under the owner's grant.
|
|
1681
|
+
const unknown = this.findCausalEventsWithoutInboxBatch(eventIds);
|
|
1682
|
+
if (unknown.length > 0) {
|
|
1683
|
+
throw new Error(`task_reclassify: causal event(s) ${unknown.join(', ')} were not carried by any owner-event inbox batch`);
|
|
1393
1684
|
}
|
|
1394
1685
|
}
|
|
1395
1686
|
}
|
|
1687
|
+
/**
|
|
1688
|
+
* A retained attempt context outlives its workorder: a late direct or nested
|
|
1689
|
+
* tool call can still carry it after the row reached a terminal status. Any
|
|
1690
|
+
* lifecycle judgment made under a carried attempt requires that attempt to be
|
|
1691
|
+
* the claimed, in-progress one, whatever its work kind.
|
|
1692
|
+
*/
|
|
1693
|
+
assertWorkOrderAttemptActive(tool, attemptId) {
|
|
1694
|
+
const attempt = this.getWorkOrderById(attemptId);
|
|
1695
|
+
if (!attempt) {
|
|
1696
|
+
throw new Error(`${tool}: workorder ${String(attemptId)} is no longer active (missing)`);
|
|
1697
|
+
}
|
|
1698
|
+
if (attempt.status !== 'in_progress') {
|
|
1699
|
+
throw new Error(`${tool}: ${attempt.workKind} workorder ${String(attemptId)} is no longer active (${attempt.status})`);
|
|
1700
|
+
}
|
|
1701
|
+
return attempt;
|
|
1702
|
+
}
|
|
1703
|
+
findCausalEventsWithoutInboxBatch(eventIds) {
|
|
1704
|
+
const placeholders = eventIds.map(() => '?').join(',');
|
|
1705
|
+
const known = new Set(this.db
|
|
1706
|
+
.prepare(`SELECT DISTINCT event.value AS event_id
|
|
1707
|
+
FROM owner_event_inbox AS inbox, json_each(inbox.event_ids_json) AS event
|
|
1708
|
+
WHERE event.value IN (${placeholders})`)
|
|
1709
|
+
.all(...eventIds).map((row) => row.event_id));
|
|
1710
|
+
return eventIds.filter((eventId) => !known.has(eventId));
|
|
1711
|
+
}
|
|
1396
1712
|
buildReclassifyPatch(existing, disposition, reason) {
|
|
1397
1713
|
const base = {
|
|
1398
1714
|
latest_event: reason,
|
|
@@ -1463,18 +1779,21 @@ class TaskLedger {
|
|
|
1463
1779
|
if (existing.kind === 'system') {
|
|
1464
1780
|
throw new Error(`task_update: task ${id} is a system workorder row (host-managed)`);
|
|
1465
1781
|
}
|
|
1466
|
-
const revisionAttempt = origin.requiresExpectedRevision
|
|
1467
|
-
? this.getWorkOrderById(origin.workOrderAttemptId ?? -1)
|
|
1468
|
-
: null;
|
|
1469
1782
|
const hasLifecyclePatch = Object.prototype.hasOwnProperty.call(patch, 'status') ||
|
|
1470
1783
|
Object.prototype.hasOwnProperty.call(patch, 'due_at') ||
|
|
1471
1784
|
Object.prototype.hasOwnProperty.call(patch, 'latest_event') ||
|
|
1472
1785
|
Object.prototype.hasOwnProperty.call(patch, 'completion_criteria');
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1786
|
+
// Direct-ABI mutation under a carried attempt: the attempt must be current
|
|
1787
|
+
// for EVERY patch shape (a stale attempt renaming a task is as stale as one
|
|
1788
|
+
// closing it). Work kind is scheduling metadata, not owner authority.
|
|
1789
|
+
// Internal receipted primitives (external lifecycle apply) do not set the
|
|
1790
|
+
// direct-ABI guard and validate their own claimed attempt separately.
|
|
1791
|
+
const isCarriedDirectMutation = origin.requiresExpectedRevision === true && origin.workOrderAttemptId !== undefined;
|
|
1792
|
+
if (isCarriedDirectMutation) {
|
|
1793
|
+
this.assertWorkOrderAttemptActive('task_update', origin.workOrderAttemptId);
|
|
1794
|
+
}
|
|
1795
|
+
// A lifecycle judgment additionally proves the revision actually read plus a reason.
|
|
1796
|
+
const isWorkorderLifecycleJudgment = isCarriedDirectMutation && hasLifecyclePatch;
|
|
1478
1797
|
if (isWorkorderLifecycleJudgment) {
|
|
1479
1798
|
if (!Number.isSafeInteger(patch.expected_revision) || patch.expected_revision < 0) {
|
|
1480
1799
|
throw new Error('task_update: lifecycle workorder requires expected_revision');
|
|
@@ -1521,6 +1840,7 @@ class TaskLedger {
|
|
|
1521
1840
|
last_temporal_attempt_id: existing.last_temporal_attempt_id,
|
|
1522
1841
|
review_started_at: existing.review_started_at,
|
|
1523
1842
|
review_anchor_event_id: existing.review_anchor_event_id,
|
|
1843
|
+
review_anchor_source_channel: existing.review_anchor_source_channel,
|
|
1524
1844
|
completion_criteria: existing.completion_criteria,
|
|
1525
1845
|
resolution_kind: existing.resolution_kind,
|
|
1526
1846
|
};
|
|
@@ -1551,6 +1871,7 @@ class TaskLedger {
|
|
|
1551
1871
|
if (leavingVerifiedReview) {
|
|
1552
1872
|
next.review_started_at = null;
|
|
1553
1873
|
next.review_anchor_event_id = null;
|
|
1874
|
+
next.review_anchor_source_channel = null;
|
|
1554
1875
|
next.due_at = null;
|
|
1555
1876
|
next.deadline = null;
|
|
1556
1877
|
next.deadline_offset_minutes = null;
|
|
@@ -1569,14 +1890,15 @@ class TaskLedger {
|
|
|
1569
1890
|
if (!evidence) {
|
|
1570
1891
|
throw new Error('task_update: review requires host-verified submission evidence');
|
|
1571
1892
|
}
|
|
1572
|
-
if (
|
|
1573
|
-
throw new Error('task_update: review evidence is
|
|
1893
|
+
if (!evidence.sourceChannel.trim()) {
|
|
1894
|
+
throw new Error('task_update: review evidence source is required');
|
|
1574
1895
|
}
|
|
1575
1896
|
if (!Number.isSafeInteger(evidence.sourceTimestampMs) || evidence.sourceTimestampMs < 0) {
|
|
1576
1897
|
throw new Error('task_update: review evidence timestamp is invalid');
|
|
1577
1898
|
}
|
|
1578
1899
|
next.review_started_at = evidence.sourceTimestampMs;
|
|
1579
1900
|
next.review_anchor_event_id = evidence.eventIndexId;
|
|
1901
|
+
next.review_anchor_source_channel = evidence.sourceChannel;
|
|
1580
1902
|
next.due_at = evidence.sourceTimestampMs + 14 * 24 * 60 * 60 * 1000;
|
|
1581
1903
|
next.deadline = new Date(next.due_at).toISOString().slice(0, 10);
|
|
1582
1904
|
next.deadline_offset_minutes = 0;
|
|
@@ -1643,6 +1965,7 @@ class TaskLedger {
|
|
|
1643
1965
|
'last_temporal_attempt_id',
|
|
1644
1966
|
'review_started_at',
|
|
1645
1967
|
'review_anchor_event_id',
|
|
1968
|
+
'review_anchor_source_channel',
|
|
1646
1969
|
'completion_criteria',
|
|
1647
1970
|
'resolution_kind',
|
|
1648
1971
|
];
|
|
@@ -1700,23 +2023,18 @@ class TaskLedger {
|
|
|
1700
2023
|
// candidate payload must remain authoritative in that case. Decision
|
|
1701
2024
|
// application still calls loadBoardCandidate(), which requires a claimed
|
|
1702
2025
|
// in-progress attempt.
|
|
2026
|
+
//
|
|
2027
|
+
// The payload's reclassificationCandidates page is NOT consulted here: it is
|
|
2028
|
+
// the scheduler's bounded-read hint, and qualifying a legacy row the owner
|
|
2029
|
+
// discovered outside that page is an ordinary owner decision guarded by
|
|
2030
|
+
// attempt currency, expected_revision and a plain reason (see
|
|
2031
|
+
// transitionTaskInTransaction). Only host-attested external binding /
|
|
2032
|
+
// lifecycle candidates keep their receipted-decision-only rule below.
|
|
1703
2033
|
if (!attempt || attempt.workKind !== 'board') {
|
|
1704
2034
|
return;
|
|
1705
2035
|
}
|
|
1706
2036
|
const { attempts: _attempts, ...payload } = attempt.payload;
|
|
1707
2037
|
(0, workorder_publishers_js_1.validateWorkOrderPayload)('board', payload);
|
|
1708
|
-
if (Object.prototype.hasOwnProperty.call(patch, 'completion_criteria') &&
|
|
1709
|
-
!origin.taskCreateUpsert) {
|
|
1710
|
-
const reclassificationCandidates = Array.isArray(payload.reclassificationCandidates)
|
|
1711
|
-
? payload.reclassificationCandidates
|
|
1712
|
-
: [];
|
|
1713
|
-
const qualificationCandidate = reclassificationCandidates.find((candidate) => candidate.taskId === taskId);
|
|
1714
|
-
if (!qualificationCandidate ||
|
|
1715
|
-
(patch.expected_revision !== undefined &&
|
|
1716
|
-
qualificationCandidate.taskRevision !== patch.expected_revision)) {
|
|
1717
|
-
throw new Error(`task_update: task ${taskId} is outside this host-issued qualification candidate set`);
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
2038
|
if (payload.mode !== 'reconcile' || !payload.candidates) {
|
|
1721
2039
|
return;
|
|
1722
2040
|
}
|
|
@@ -1966,7 +2284,9 @@ class TaskLedger {
|
|
|
1966
2284
|
currentOccurrence !== input.occurrenceKey) {
|
|
1967
2285
|
throw new Error(`temporal generation: task occurrence no longer matches enqueue input`);
|
|
1968
2286
|
}
|
|
1969
|
-
if (this.temporalSourceIdentifierRef(task.
|
|
2287
|
+
if (this.temporalSourceIdentifierRef(task.status === 'review'
|
|
2288
|
+
? (task.reviewAnchorSourceChannel ?? task.sourceChannel)
|
|
2289
|
+
: task.sourceChannel) !== sourceChannelRef ||
|
|
1970
2290
|
this.temporalSourceIdentifierRef(task.status === 'review' && task.reviewAnchorEventId
|
|
1971
2291
|
? task.reviewAnchorEventId
|
|
1972
2292
|
: task.sourceEventId) !== sourceEventIdRef) {
|
|
@@ -2328,6 +2648,7 @@ class TaskLedger {
|
|
|
2328
2648
|
throw new Error('temporal effect requires a valid host evidence attestation');
|
|
2329
2649
|
}
|
|
2330
2650
|
let receipt = null;
|
|
2651
|
+
const previousGeneration = this.onOwnerTaskChangeCommitted ? this.readGeneration() : null;
|
|
2331
2652
|
this.db.exec('BEGIN IMMEDIATE');
|
|
2332
2653
|
try {
|
|
2333
2654
|
const context = this.assertTemporalWorkContextActive(suppliedContext);
|
|
@@ -2352,6 +2673,7 @@ class TaskLedger {
|
|
|
2352
2673
|
last_temporal_attempt_id: context.attemptId,
|
|
2353
2674
|
review_started_at: existing.review_started_at,
|
|
2354
2675
|
review_anchor_event_id: existing.review_anchor_event_id,
|
|
2676
|
+
review_anchor_source_channel: existing.review_anchor_source_channel,
|
|
2355
2677
|
};
|
|
2356
2678
|
const auditParts = [`reason=${input.reason.trim()}`];
|
|
2357
2679
|
if (input.outcome === 'final_no_update') {
|
|
@@ -2369,6 +2691,7 @@ class TaskLedger {
|
|
|
2369
2691
|
// as the task_update path clears them. A later reopen re-verifies.
|
|
2370
2692
|
next.review_started_at = null;
|
|
2371
2693
|
next.review_anchor_event_id = null;
|
|
2694
|
+
next.review_anchor_source_channel = null;
|
|
2372
2695
|
if (existing.review_started_at !== null &&
|
|
2373
2696
|
!Object.prototype.hasOwnProperty.call(input, 'due_at')) {
|
|
2374
2697
|
next.due_at = null;
|
|
@@ -2417,6 +2740,7 @@ class TaskLedger {
|
|
|
2417
2740
|
'last_temporal_attempt_id',
|
|
2418
2741
|
'review_started_at',
|
|
2419
2742
|
'review_anchor_event_id',
|
|
2743
|
+
'review_anchor_source_channel',
|
|
2420
2744
|
];
|
|
2421
2745
|
const changedFields = persistedColumns.filter((column) => next[column] !== existing[column]);
|
|
2422
2746
|
const afterRevision = existing.revision + 1;
|
|
@@ -2503,6 +2827,7 @@ class TaskLedger {
|
|
|
2503
2827
|
this.db.exec('ROLLBACK');
|
|
2504
2828
|
throw error;
|
|
2505
2829
|
}
|
|
2830
|
+
this.notifyOwnerTaskCommit(previousGeneration);
|
|
2506
2831
|
if (!receipt)
|
|
2507
2832
|
throw new Error(`temporal effect produced no receipt`);
|
|
2508
2833
|
return receipt;
|
|
@@ -2912,7 +3237,9 @@ class TaskLedger {
|
|
|
2912
3237
|
}
|
|
2913
3238
|
if (task.temporalEpoch !== temporalEpoch ||
|
|
2914
3239
|
(0, task_temporal_js_1.occurrenceKeyForTask)(task) !== occurrenceKey ||
|
|
2915
|
-
this.temporalSourceIdentifierRef(task.
|
|
3240
|
+
this.temporalSourceIdentifierRef(task.status === 'review'
|
|
3241
|
+
? (task.reviewAnchorSourceChannel ?? task.sourceChannel)
|
|
3242
|
+
: task.sourceChannel) !== sourceChannel ||
|
|
2916
3243
|
this.temporalSourceIdentifierRef(task.status === 'review' && task.reviewAnchorEventId
|
|
2917
3244
|
? task.reviewAnchorEventId
|
|
2918
3245
|
: task.sourceEventId) !== sourceEventId ||
|