@songsid/agend 2.1.7-beta.4 → 2.1.7-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/README.zh-TW.md +2 -0
- package/dist/agent-cli-instructions.md +5 -0
- package/dist/agent-cli.js +13 -0
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +1 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/backend/codex-session-lookup.d.ts +59 -0
- package/dist/backend/codex-session-lookup.js +139 -0
- package/dist/backend/codex-session-lookup.js.map +1 -0
- package/dist/backend/codex.d.ts +11 -0
- package/dist/backend/codex.js +115 -7
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/types.d.ts +12 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/mcp-tools.js +2 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +68 -2
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +11 -0
- package/dist/daemon.js +173 -23
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +90 -6
- package/dist/delivery-outbox.js +301 -39
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-outbox.test.js +268 -14
- package/dist/delivery-outbox.test.js.map +1 -1
- package/dist/delivery-queue-evidence.d.ts +15 -0
- package/dist/delivery-queue-evidence.js +35 -0
- package/dist/delivery-queue-evidence.js.map +1 -0
- package/dist/delivery-queue-evidence.test.d.ts +1 -0
- package/dist/delivery-queue-evidence.test.js +23 -0
- package/dist/delivery-queue-evidence.test.js.map +1 -0
- package/dist/delivery-reconciliation.d.ts +49 -0
- package/dist/delivery-reconciliation.js +296 -0
- package/dist/delivery-reconciliation.js.map +1 -0
- package/dist/delivery-reconciliation.test.d.ts +1 -0
- package/dist/delivery-reconciliation.test.js +411 -0
- package/dist/delivery-reconciliation.test.js.map +1 -0
- package/dist/fleet-manager.d.ts +2 -1
- package/dist/fleet-manager.js +61 -32
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +13 -0
- package/dist/instance-lifecycle.js +85 -10
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +1 -0
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +6 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.d.ts +3 -0
- package/dist/outbound-handlers.js +38 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +7 -0
- package/dist/outbound-schemas.js +14 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/pane-write-lock.d.ts +2 -0
- package/dist/pane-write-lock.js +19 -0
- package/dist/pane-write-lock.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +5 -1
- package/dist/scheduler/scheduler.js +60 -20
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/scheduler/scheduler.test.js +41 -2
- package/dist/scheduler/scheduler.test.js.map +1 -1
- package/dist/scheduler/types.d.ts +1 -1
- package/dist/tmux-manager.d.ts +3 -3
- package/dist/tmux-manager.js +6 -6
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.js +6 -6
- package/dist/tool-permissions.js.map +1 -1
- package/dist/transcript-monitor.d.ts +3 -1
- package/dist/transcript-monitor.js +15 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +9 -0
- package/dist/transcript-sources.js +14 -0
- package/dist/transcript-sources.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { QueueResumePolicy } from "./delivery-queue-evidence.js";
|
|
2
3
|
export declare const DURABLE_DELIVERY_MAX_ATTEMPTS = 8;
|
|
3
4
|
export declare const DURABLE_DELIVERY_MAX_AGE_MS: number;
|
|
4
5
|
export declare const DURABLE_DELIVERY_ABORT_BACKOFF_MS = 5000;
|
|
5
6
|
export interface DaemonDeliveryPort {
|
|
6
|
-
begin(deliveryId: string, targetBootId: string, attemptNo: number): "begun" | "duplicate" | "stale";
|
|
7
|
+
begin(deliveryId: string, targetBootId: string, attemptNo: number, evidence?: DeliveryAttemptEvidence): "begun" | "duplicate" | "stale";
|
|
8
|
+
markEnterStarted(deliveryId: string, targetBootId: string, attemptNo: number): boolean;
|
|
7
9
|
abort(deliveryId: string, targetBootId: string, attemptNo: number, reason: string): boolean;
|
|
8
10
|
complete(deliveryId: string, targetBootId: string, attemptNo: number, outcome: "delivered" | "failed" | "uncertain", evidence?: string): boolean;
|
|
9
11
|
retryBeforeBegin(deliveryId: string, targetBootId: string, attemptNo: number, reason: string, delayMs?: number): boolean;
|
|
10
12
|
}
|
|
11
|
-
export type OutboxState = "queued" | "delivering" | "submission_started" | "retry_wait" | "delivered" | "failed" | "uncertain" | "cancelled";
|
|
13
|
+
export type OutboxState = "queued" | "delivering" | "submission_started" | "reconciliation_pending" | "retry_wait" | "delivered" | "failed" | "uncertain" | "cancelled";
|
|
12
14
|
export interface NewOutboxDelivery {
|
|
13
15
|
operationId: string;
|
|
14
16
|
sourceKey: string;
|
|
@@ -39,6 +41,72 @@ export interface OutboxDelivery {
|
|
|
39
41
|
responseDeliveredAt: string | null;
|
|
40
42
|
nextAttemptAt: string | null;
|
|
41
43
|
lastError: string | null;
|
|
44
|
+
reconciliationPending: boolean;
|
|
45
|
+
}
|
|
46
|
+
export type DeliveryStatusSelector = {
|
|
47
|
+
deliveryId: string;
|
|
48
|
+
operationId?: never;
|
|
49
|
+
correlationId?: never;
|
|
50
|
+
limit?: never;
|
|
51
|
+
cursor?: never;
|
|
52
|
+
} | {
|
|
53
|
+
operationId: string;
|
|
54
|
+
deliveryId?: never;
|
|
55
|
+
correlationId?: never;
|
|
56
|
+
limit?: number;
|
|
57
|
+
cursor?: string;
|
|
58
|
+
} | {
|
|
59
|
+
correlationId: string;
|
|
60
|
+
deliveryId?: never;
|
|
61
|
+
operationId?: never;
|
|
62
|
+
limit?: number;
|
|
63
|
+
cursor?: string;
|
|
64
|
+
};
|
|
65
|
+
export interface DeliveryStatusItem {
|
|
66
|
+
delivery_id: string;
|
|
67
|
+
operation_id: string;
|
|
68
|
+
correlation_id: string | null;
|
|
69
|
+
source_instance: string;
|
|
70
|
+
target_instance: string;
|
|
71
|
+
kind: string;
|
|
72
|
+
state: OutboxState;
|
|
73
|
+
attempt_no: number;
|
|
74
|
+
created_at: string;
|
|
75
|
+
updated_at: string;
|
|
76
|
+
status_summary: string;
|
|
77
|
+
error_summary: string | null;
|
|
78
|
+
safe_to_retry: boolean;
|
|
79
|
+
}
|
|
80
|
+
export interface DeliveryStatusPage {
|
|
81
|
+
items: DeliveryStatusItem[];
|
|
82
|
+
next_cursor: string | null;
|
|
83
|
+
}
|
|
84
|
+
export type DurableSubmissionMode = "idle_submit" | "native_queue_handoff" | "steer" | "raw_paste";
|
|
85
|
+
/** Checkpoint committed with begin, before any pane paste can occur. */
|
|
86
|
+
export interface DeliveryAttemptEvidence {
|
|
87
|
+
backend: string;
|
|
88
|
+
backendVersion?: string | null;
|
|
89
|
+
windowId: string | null;
|
|
90
|
+
transcriptPath: string | null;
|
|
91
|
+
transcriptOffset: number | null;
|
|
92
|
+
transcriptSessionId: string | null;
|
|
93
|
+
submissionMode: DurableSubmissionMode;
|
|
94
|
+
queueResumePolicy?: QueueResumePolicy;
|
|
95
|
+
}
|
|
96
|
+
export interface DeliveryReconciliationCandidate extends OutboxDelivery {
|
|
97
|
+
attempt: {
|
|
98
|
+
targetDaemonBootId: string;
|
|
99
|
+
attemptNo: number;
|
|
100
|
+
backend: string | null;
|
|
101
|
+
backendVersion: string | null;
|
|
102
|
+
windowId: string | null;
|
|
103
|
+
transcriptPath: string | null;
|
|
104
|
+
transcriptOffset: number | null;
|
|
105
|
+
transcriptSessionId: string | null;
|
|
106
|
+
submissionMode: DurableSubmissionMode | null;
|
|
107
|
+
queueResumePolicy: QueueResumePolicy | null;
|
|
108
|
+
enterStartedAt: string | null;
|
|
109
|
+
};
|
|
42
110
|
}
|
|
43
111
|
export interface ClaimedOutboxDelivery extends OutboxDelivery {
|
|
44
112
|
targetDaemonBootId: string;
|
|
@@ -48,6 +116,7 @@ export declare class DeliveryOutbox extends EventEmitter {
|
|
|
48
116
|
private readonly db;
|
|
49
117
|
readonly managerBootId: string;
|
|
50
118
|
constructor(dbPath: string, managerBootId?: string);
|
|
119
|
+
private ensureColumn;
|
|
51
120
|
/** Commit admission before returning success. Duplicate source keys return their original row. */
|
|
52
121
|
admit(input: NewOutboxDelivery): {
|
|
53
122
|
delivery: OutboxDelivery;
|
|
@@ -55,19 +124,29 @@ export declare class DeliveryOutbox extends EventEmitter {
|
|
|
55
124
|
};
|
|
56
125
|
get(deliveryId: string): OutboxDelivery | undefined;
|
|
57
126
|
getByOperation(sourceInstance: string, operationId: string): OutboxDelivery[];
|
|
127
|
+
/**
|
|
128
|
+
* Query only rows owned by the authenticated instance as source or target.
|
|
129
|
+
* This method accepts caller identity separately from user arguments so a
|
|
130
|
+
* caller cannot widen its own visibility with a forged source name.
|
|
131
|
+
*/
|
|
132
|
+
queryStatusForInstance(callerInstance: string, selector: DeliveryStatusSelector): DeliveryStatusPage;
|
|
133
|
+
/** Open the existing outbox without migrations or writes for the operator CLI. */
|
|
134
|
+
static queryStatusReadOnly(dbPath: string, selector: DeliveryStatusSelector): DeliveryStatusPage;
|
|
58
135
|
listPending(): OutboxDelivery[];
|
|
59
136
|
nextRetryAt(): string | null;
|
|
60
137
|
/** Claim the earliest eligible target row; one caller owns each target lane. */
|
|
61
138
|
claimNext(managerBootId: string, targetBootIdFor: (target: string) => string | null, blockedTargets: ReadonlySet<string>): ClaimedOutboxDelivery | undefined;
|
|
62
139
|
/** Idempotent begin permit, committed immediately before the pane side effect. */
|
|
63
|
-
begin(deliveryId: string, targetBootId: string, attemptNo: number): "begun" | "duplicate" | "stale";
|
|
140
|
+
begin(deliveryId: string, targetBootId: string, attemptNo: number, evidence?: DeliveryAttemptEvidence): "begun" | "duplicate" | "stale";
|
|
141
|
+
/** Write-ahead fence: commit before tmux is allowed to receive Enter. */
|
|
142
|
+
markEnterStarted(deliveryId: string, targetBootId: string, attemptNo: number): boolean;
|
|
64
143
|
/** Safe only when the caller can prove no pane write occurred for this attempt. */
|
|
65
144
|
abort(deliveryId: string, targetBootId: string, attemptNo: number, reason: string): boolean;
|
|
66
145
|
complete(deliveryId: string, targetBootId: string, attemptNo: number, outcome: "delivered" | "failed" | "uncertain", evidence?: string): boolean;
|
|
67
146
|
/** Transient pre-submit failure: state is retryable and the same row keeps its FIFO position. */
|
|
68
147
|
retryBeforeBegin(deliveryId: string, targetBootId: string, attemptNo: number, reason: string, delayMs?: number): boolean;
|
|
69
148
|
failBeforeBegin(deliveryId: string, reason: string): boolean;
|
|
70
|
-
/**
|
|
149
|
+
/** Bound both unavailable rows and unresolved reconciliation without stealing a live lease. */
|
|
71
150
|
expireStale(nowMs?: number, maxAgeMs?: number): number;
|
|
72
151
|
/** Next queued/retry expiry for bounded maintenance scheduling. */
|
|
73
152
|
nextExpiryAt(): string | null;
|
|
@@ -75,14 +154,19 @@ export declare class DeliveryOutbox extends EventEmitter {
|
|
|
75
154
|
/** Old process leases are recoverable; an old submission attempt needs evidence reconciliation. */
|
|
76
155
|
recoverForBoot(managerBootId: string): {
|
|
77
156
|
queued: number;
|
|
78
|
-
|
|
157
|
+
reconciliationPending: number;
|
|
79
158
|
};
|
|
80
159
|
/** Atomically create a post-restart notice for an accepted operation whose response was not recorded. */
|
|
81
160
|
admitPostRestartOutcomeNotice(parent: OutboxDelivery, currentSourceBootId: string): OutboxDelivery | undefined;
|
|
82
161
|
recoverTargetGeneration(targetInstance: string, currentTargetBootId: string): {
|
|
83
162
|
queued: number;
|
|
84
|
-
|
|
163
|
+
reconciliationPending: number;
|
|
85
164
|
};
|
|
165
|
+
/** Fence in-flight writes before an in-process target stop begins. */
|
|
166
|
+
markTargetReconciliationPending(targetInstance: string, targetBootId: string): number;
|
|
167
|
+
getReconciliationCandidates(targetInstance: string): DeliveryReconciliationCandidate[];
|
|
168
|
+
/** Atomically settle one old attempt using only its boot/attempt fence. */
|
|
169
|
+
reconcileAttempt(deliveryId: string, targetBootId: string, attemptNo: number, outcome: "delivered" | "retry_wait" | "uncertain", evidence: string): boolean;
|
|
86
170
|
getUnansweredAccepted(sourceInstance: string, currentBootId: string): OutboxDelivery[];
|
|
87
171
|
close(): void;
|
|
88
172
|
/** Must be called inside the same SQLite transaction as the terminal transition. */
|
package/dist/delivery-outbox.js
CHANGED
|
@@ -19,13 +19,115 @@ function mapRow(row) {
|
|
|
19
19
|
kind: row.kind,
|
|
20
20
|
correlationId: row.correlation_id,
|
|
21
21
|
payload: JSON.parse(row.payload_json),
|
|
22
|
-
state: row.state
|
|
22
|
+
state: row.state === "submission_started" && row.reconciliation_pending === 1
|
|
23
|
+
? "reconciliation_pending"
|
|
24
|
+
: row.state,
|
|
23
25
|
attemptNo: row.attempt_no,
|
|
24
26
|
createdSeq: row.created_seq,
|
|
25
27
|
managerBootId: row.manager_boot_id,
|
|
26
28
|
responseDeliveredAt: row.response_delivered_at,
|
|
27
29
|
nextAttemptAt: row.next_attempt_at,
|
|
28
30
|
lastError: row.last_error,
|
|
31
|
+
reconciliationPending: row.reconciliation_pending === 1,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function safeErrorSummary(error) {
|
|
35
|
+
if (!error)
|
|
36
|
+
return null;
|
|
37
|
+
const normalized = error.toLowerCase();
|
|
38
|
+
if (normalized.includes("ttl expired"))
|
|
39
|
+
return "Delivery expired before the target became available.";
|
|
40
|
+
if (normalized.includes("attempt limit"))
|
|
41
|
+
return "Delivery exhausted its bounded retry attempts.";
|
|
42
|
+
if (normalized.includes("reconcil") || normalized.includes("uncertain") || normalized.includes("restart")) {
|
|
43
|
+
return "Submission may have occurred; do not resend until the outcome is reconciled.";
|
|
44
|
+
}
|
|
45
|
+
return "A delivery error was recorded; details are omitted for safety.";
|
|
46
|
+
}
|
|
47
|
+
function statusSummary(state) {
|
|
48
|
+
switch (state) {
|
|
49
|
+
case "queued": return "Accepted and waiting for delivery.";
|
|
50
|
+
case "delivering": return "Waiting for the target to become ready; submission has not started.";
|
|
51
|
+
case "retry_wait": return "Waiting for a bounded retry; submission has not started for the next attempt.";
|
|
52
|
+
case "submission_started":
|
|
53
|
+
case "reconciliation_pending": return "Submission may have occurred; do not resend.";
|
|
54
|
+
case "delivered": return "Delivery was confirmed; this does not mean the agent finished processing it.";
|
|
55
|
+
case "failed": return "Delivery failed after bounded retries or expiry.";
|
|
56
|
+
case "uncertain": return "Delivery may have occurred; do not resend blindly.";
|
|
57
|
+
case "cancelled": return "Delivery was cancelled.";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function safeToRetry(state) {
|
|
61
|
+
return state === "queued" || state === "delivering" || state === "retry_wait" || state === "failed";
|
|
62
|
+
}
|
|
63
|
+
function queryStatusPage(db, selector, callerInstance) {
|
|
64
|
+
const where = [];
|
|
65
|
+
const params = [];
|
|
66
|
+
if (selector.deliveryId) {
|
|
67
|
+
where.push("d.delivery_id = ?");
|
|
68
|
+
params.push(selector.deliveryId);
|
|
69
|
+
}
|
|
70
|
+
else if (selector.operationId) {
|
|
71
|
+
where.push("d.operation_id = ?");
|
|
72
|
+
params.push(selector.operationId);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
where.push("d.correlation_id = ?");
|
|
76
|
+
params.push(selector.correlationId);
|
|
77
|
+
}
|
|
78
|
+
if (callerInstance !== null) {
|
|
79
|
+
where.push("(d.source_instance = ? OR d.target_instance = ?)");
|
|
80
|
+
params.push(callerInstance, callerInstance);
|
|
81
|
+
}
|
|
82
|
+
if (selector.cursor) {
|
|
83
|
+
const cursorScope = ["c.delivery_id = ?"];
|
|
84
|
+
const cursorParams = [selector.cursor];
|
|
85
|
+
if (selector.operationId) {
|
|
86
|
+
cursorScope.push("c.operation_id = ?");
|
|
87
|
+
cursorParams.push(selector.operationId);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
cursorScope.push("c.correlation_id = ?");
|
|
91
|
+
cursorParams.push(selector.correlationId);
|
|
92
|
+
}
|
|
93
|
+
if (callerInstance !== null) {
|
|
94
|
+
cursorScope.push("(c.source_instance = ? OR c.target_instance = ?)");
|
|
95
|
+
cursorParams.push(callerInstance, callerInstance);
|
|
96
|
+
}
|
|
97
|
+
where.push(`d.created_seq > (SELECT c.created_seq FROM deliveries c WHERE ${cursorScope.join(" AND ")} LIMIT 1)`);
|
|
98
|
+
params.push(...cursorParams);
|
|
99
|
+
}
|
|
100
|
+
const limit = selector.deliveryId ? 1 : Math.max(1, Math.min(100, selector.limit ?? 20));
|
|
101
|
+
const rows = db.prepare(`
|
|
102
|
+
SELECT d.delivery_id,d.operation_id,d.correlation_id,d.source_instance,d.target_instance,
|
|
103
|
+
d.kind,
|
|
104
|
+
CASE WHEN d.state='submission_started' AND d.reconciliation_pending=1
|
|
105
|
+
THEN 'reconciliation_pending' ELSE d.state END AS state,
|
|
106
|
+
d.attempt_no,d.created_at,d.updated_at,d.last_error
|
|
107
|
+
FROM deliveries d
|
|
108
|
+
WHERE ${where.join(" AND ")}
|
|
109
|
+
ORDER BY d.created_seq
|
|
110
|
+
LIMIT ?
|
|
111
|
+
`).all(...params, limit + 1);
|
|
112
|
+
const hasMore = rows.length > limit;
|
|
113
|
+
const visible = rows.slice(0, limit);
|
|
114
|
+
return {
|
|
115
|
+
items: visible.map(row => ({
|
|
116
|
+
delivery_id: row.delivery_id,
|
|
117
|
+
operation_id: row.operation_id,
|
|
118
|
+
correlation_id: row.correlation_id,
|
|
119
|
+
source_instance: row.source_instance,
|
|
120
|
+
target_instance: row.target_instance,
|
|
121
|
+
kind: row.kind,
|
|
122
|
+
state: row.state,
|
|
123
|
+
attempt_no: row.attempt_no,
|
|
124
|
+
created_at: row.created_at,
|
|
125
|
+
updated_at: row.updated_at,
|
|
126
|
+
status_summary: statusSummary(row.state),
|
|
127
|
+
error_summary: safeErrorSummary(row.last_error),
|
|
128
|
+
safe_to_retry: safeToRetry(row.state),
|
|
129
|
+
})),
|
|
130
|
+
next_cursor: hasMore ? visible.at(-1)?.delivery_id ?? null : null,
|
|
29
131
|
};
|
|
30
132
|
}
|
|
31
133
|
/** Durable admission and fenced state transitions for the fleet delivery queue. */
|
|
@@ -63,6 +165,7 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
63
165
|
created_seq INTEGER NOT NULL UNIQUE,
|
|
64
166
|
manager_boot_id TEXT,
|
|
65
167
|
response_delivered_at TEXT,
|
|
168
|
+
reconciliation_pending INTEGER NOT NULL DEFAULT 0,
|
|
66
169
|
next_attempt_at TEXT,
|
|
67
170
|
last_error TEXT,
|
|
68
171
|
created_at TEXT NOT NULL,
|
|
@@ -77,6 +180,10 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
77
180
|
ON deliveries(target_instance, state, created_seq);
|
|
78
181
|
CREATE INDEX IF NOT EXISTS idx_delivery_operation_source
|
|
79
182
|
ON deliveries(source_instance, operation_id);
|
|
183
|
+
CREATE INDEX IF NOT EXISTS idx_delivery_operation_seq
|
|
184
|
+
ON deliveries(operation_id, created_seq);
|
|
185
|
+
CREATE INDEX IF NOT EXISTS idx_delivery_correlation_seq
|
|
186
|
+
ON deliveries(correlation_id, created_seq);
|
|
80
187
|
|
|
81
188
|
CREATE TABLE IF NOT EXISTS delivery_attempts (
|
|
82
189
|
delivery_id TEXT NOT NULL REFERENCES deliveries(delivery_id),
|
|
@@ -87,6 +194,15 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
87
194
|
abort_at TEXT,
|
|
88
195
|
finished_at TEXT,
|
|
89
196
|
evidence TEXT,
|
|
197
|
+
backend TEXT,
|
|
198
|
+
window_id TEXT,
|
|
199
|
+
transcript_path TEXT,
|
|
200
|
+
transcript_offset INTEGER,
|
|
201
|
+
transcript_session_id TEXT,
|
|
202
|
+
submission_mode TEXT,
|
|
203
|
+
backend_version TEXT,
|
|
204
|
+
queue_resume_policy TEXT,
|
|
205
|
+
enter_started_at TEXT,
|
|
90
206
|
PRIMARY KEY (delivery_id, target_daemon_boot_id, attempt_no)
|
|
91
207
|
);
|
|
92
208
|
|
|
@@ -104,8 +220,26 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
104
220
|
notice_delivery_id TEXT NOT NULL UNIQUE,
|
|
105
221
|
created_at TEXT NOT NULL
|
|
106
222
|
);
|
|
107
|
-
PRAGMA user_version = 1;
|
|
108
223
|
`);
|
|
224
|
+
// Additive migration: existing Phase 1 databases keep their row identities,
|
|
225
|
+
// foreign keys, and WAL while gaining reconciliation evidence columns.
|
|
226
|
+
this.ensureColumn("deliveries", "reconciliation_pending", "INTEGER NOT NULL DEFAULT 0");
|
|
227
|
+
this.ensureColumn("delivery_attempts", "backend", "TEXT");
|
|
228
|
+
this.ensureColumn("delivery_attempts", "window_id", "TEXT");
|
|
229
|
+
this.ensureColumn("delivery_attempts", "transcript_path", "TEXT");
|
|
230
|
+
this.ensureColumn("delivery_attempts", "transcript_offset", "INTEGER");
|
|
231
|
+
this.ensureColumn("delivery_attempts", "transcript_session_id", "TEXT");
|
|
232
|
+
this.ensureColumn("delivery_attempts", "submission_mode", "TEXT");
|
|
233
|
+
this.ensureColumn("delivery_attempts", "backend_version", "TEXT");
|
|
234
|
+
this.ensureColumn("delivery_attempts", "queue_resume_policy", "TEXT");
|
|
235
|
+
this.ensureColumn("delivery_attempts", "enter_started_at", "TEXT");
|
|
236
|
+
this.db.pragma("user_version = 3");
|
|
237
|
+
}
|
|
238
|
+
ensureColumn(table, column, definition) {
|
|
239
|
+
const columns = this.db.prepare(`PRAGMA table_info(${table})`).all();
|
|
240
|
+
if (columns.some(item => item.name === column))
|
|
241
|
+
return;
|
|
242
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
109
243
|
}
|
|
110
244
|
/** Commit admission before returning success. Duplicate source keys return their original row. */
|
|
111
245
|
admit(input) {
|
|
@@ -157,6 +291,26 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
157
291
|
`).all(sourceInstance, operationId);
|
|
158
292
|
return rows.map(mapRow);
|
|
159
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Query only rows owned by the authenticated instance as source or target.
|
|
296
|
+
* This method accepts caller identity separately from user arguments so a
|
|
297
|
+
* caller cannot widen its own visibility with a forged source name.
|
|
298
|
+
*/
|
|
299
|
+
queryStatusForInstance(callerInstance, selector) {
|
|
300
|
+
return queryStatusPage(this.db, selector, callerInstance);
|
|
301
|
+
}
|
|
302
|
+
/** Open the existing outbox without migrations or writes for the operator CLI. */
|
|
303
|
+
static queryStatusReadOnly(dbPath, selector) {
|
|
304
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
305
|
+
try {
|
|
306
|
+
db.pragma("busy_timeout = 1000");
|
|
307
|
+
db.pragma("query_only = ON");
|
|
308
|
+
return queryStatusPage(db, selector, null);
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
db.close();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
160
314
|
listPending() {
|
|
161
315
|
const rows = this.db.prepare(`
|
|
162
316
|
SELECT * FROM deliveries
|
|
@@ -182,6 +336,10 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
182
336
|
// The oldest pending row owns its target lane even while it is in retry
|
|
183
337
|
// backoff or the target is temporarily unavailable. Later rows must not
|
|
184
338
|
// pass it merely because they happen to be eligible first.
|
|
339
|
+
const reconciliationTargets = new Set(this.db.prepare(`
|
|
340
|
+
SELECT DISTINCT target_instance FROM deliveries
|
|
341
|
+
WHERE state='submission_started' AND reconciliation_pending=1
|
|
342
|
+
`).all().map(item => item.target_instance));
|
|
185
343
|
const visitedTargets = new Set();
|
|
186
344
|
for (const row of rows) {
|
|
187
345
|
if (visitedTargets.has(row.target_instance))
|
|
@@ -189,6 +347,10 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
189
347
|
visitedTargets.add(row.target_instance);
|
|
190
348
|
if (blockedTargets.has(row.target_instance))
|
|
191
349
|
continue;
|
|
350
|
+
// A recovered submission owns only its target lane while its evidence is
|
|
351
|
+
// classified; independent targets can keep dispatching.
|
|
352
|
+
if (reconciliationTargets.has(row.target_instance))
|
|
353
|
+
continue;
|
|
192
354
|
if (row.state === "retry_wait" && row.next_attempt_at && row.next_attempt_at > now)
|
|
193
355
|
continue;
|
|
194
356
|
const targetBootId = targetBootIdFor(row.target_instance);
|
|
@@ -210,7 +372,7 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
210
372
|
return undefined;
|
|
211
373
|
}
|
|
212
374
|
/** Idempotent begin permit, committed immediately before the pane side effect. */
|
|
213
|
-
begin(deliveryId, targetBootId, attemptNo) {
|
|
375
|
+
begin(deliveryId, targetBootId, attemptNo, evidence) {
|
|
214
376
|
const now = new Date().toISOString();
|
|
215
377
|
const transaction = this.db.transaction(() => {
|
|
216
378
|
const row = this.db.prepare("SELECT * FROM deliveries WHERE delivery_id=?").get(deliveryId);
|
|
@@ -239,9 +401,12 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
239
401
|
return { outcome: "stale", failed: true };
|
|
240
402
|
}
|
|
241
403
|
this.db.prepare(`
|
|
242
|
-
INSERT INTO delivery_attempts(
|
|
243
|
-
|
|
244
|
-
|
|
404
|
+
INSERT INTO delivery_attempts(
|
|
405
|
+
delivery_id,target_daemon_boot_id,attempt_no,state,begin_ack_at,
|
|
406
|
+
backend,window_id,transcript_path,transcript_offset,transcript_session_id,submission_mode,
|
|
407
|
+
backend_version,queue_resume_policy
|
|
408
|
+
) VALUES (?,?,?,'begun',?,?,?,?,?,?,?,?,?)
|
|
409
|
+
`).run(deliveryId, targetBootId, attemptNo, now, evidence?.backend ?? null, evidence?.windowId ?? null, evidence?.transcriptPath ?? null, evidence?.transcriptOffset ?? null, evidence?.transcriptSessionId ?? null, evidence?.submissionMode ?? null, evidence?.backendVersion ?? null, evidence?.queueResumePolicy ?? null);
|
|
245
410
|
this.db.prepare(`
|
|
246
411
|
UPDATE deliveries SET state='submission_started', submitted_at=?, updated_at=?
|
|
247
412
|
WHERE delivery_id=? AND state='delivering'
|
|
@@ -255,6 +420,19 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
255
420
|
this.emit("state", { deliveryId, state: "failed" });
|
|
256
421
|
return result.outcome;
|
|
257
422
|
}
|
|
423
|
+
/** Write-ahead fence: commit before tmux is allowed to receive Enter. */
|
|
424
|
+
markEnterStarted(deliveryId, targetBootId, attemptNo) {
|
|
425
|
+
const now = new Date().toISOString();
|
|
426
|
+
const changed = this.db.prepare(`
|
|
427
|
+
UPDATE delivery_attempts SET enter_started_at=COALESCE(enter_started_at,?)
|
|
428
|
+
WHERE delivery_id=? AND target_daemon_boot_id=? AND attempt_no=? AND state='begun'
|
|
429
|
+
AND EXISTS (
|
|
430
|
+
SELECT 1 FROM deliveries WHERE delivery_id=? AND target_daemon_boot_id=?
|
|
431
|
+
AND attempt_no=? AND state='submission_started'
|
|
432
|
+
)
|
|
433
|
+
`).run(now, deliveryId, targetBootId, attemptNo, deliveryId, targetBootId, attemptNo).changes;
|
|
434
|
+
return changed === 1;
|
|
435
|
+
}
|
|
258
436
|
/** Safe only when the caller can prove no pane write occurred for this attempt. */
|
|
259
437
|
abort(deliveryId, targetBootId, attemptNo, reason) {
|
|
260
438
|
const now = new Date().toISOString();
|
|
@@ -281,7 +459,7 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
281
459
|
if (updateAttempt.changes !== 1)
|
|
282
460
|
return false;
|
|
283
461
|
const updateDelivery = this.db.prepare(`
|
|
284
|
-
UPDATE deliveries SET state='retry_wait',updated_at=?,last_error=?,next_attempt_at=?
|
|
462
|
+
UPDATE deliveries SET state='retry_wait',reconciliation_pending=0,updated_at=?,last_error=?,next_attempt_at=?
|
|
285
463
|
WHERE delivery_id=? AND state='submission_started' AND target_daemon_boot_id=? AND attempt_no=?
|
|
286
464
|
`).run(now, reason.slice(0, 300), new Date(Date.now() + DURABLE_DELIVERY_ABORT_BACKOFF_MS).toISOString(), deliveryId, targetBootId, attemptNo);
|
|
287
465
|
if (updateDelivery.changes !== 1)
|
|
@@ -298,7 +476,7 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
298
476
|
const transaction = this.db.transaction(() => {
|
|
299
477
|
const parent = this.db.prepare("SELECT * FROM deliveries WHERE delivery_id=?").get(deliveryId);
|
|
300
478
|
const result = this.db.prepare(`
|
|
301
|
-
UPDATE deliveries SET state=?,updated_at=?,finished_at=?,last_error=?
|
|
479
|
+
UPDATE deliveries SET state=?,reconciliation_pending=0,updated_at=?,finished_at=?,last_error=?
|
|
302
480
|
WHERE delivery_id=? AND target_daemon_boot_id=? AND attempt_no=? AND state='submission_started'
|
|
303
481
|
`).run(outcome, now, now, outcome === "delivered" ? null : (evidence ?? outcome).slice(0, 300), deliveryId, targetBootId, attemptNo);
|
|
304
482
|
if (result.changes !== 1) {
|
|
@@ -368,7 +546,7 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
368
546
|
this.emit("state", { deliveryId, state: "failed" });
|
|
369
547
|
return changed;
|
|
370
548
|
}
|
|
371
|
-
/**
|
|
549
|
+
/** Bound both unavailable rows and unresolved reconciliation without stealing a live lease. */
|
|
372
550
|
expireStale(nowMs = Date.now(), maxAgeMs = DURABLE_DELIVERY_MAX_AGE_MS) {
|
|
373
551
|
const cutoff = new Date(nowMs - maxAgeMs).toISOString();
|
|
374
552
|
const now = new Date(nowMs).toISOString();
|
|
@@ -383,23 +561,46 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
383
561
|
WHERE delivery_id=? AND state IN ('queued','retry_wait')
|
|
384
562
|
`);
|
|
385
563
|
let changed = 0;
|
|
564
|
+
let uncertain = 0;
|
|
386
565
|
for (const row of stale) {
|
|
387
566
|
if (update.run(now, now, row.delivery_id).changes !== 1)
|
|
388
567
|
continue;
|
|
389
568
|
this.insertFailureNotice(row, "failed", "delivery TTL expired before target became available", now);
|
|
390
569
|
changed++;
|
|
391
570
|
}
|
|
392
|
-
|
|
571
|
+
const unresolved = this.db.prepare(`
|
|
572
|
+
SELECT * FROM deliveries
|
|
573
|
+
WHERE state='submission_started' AND reconciliation_pending=1 AND created_at<=?
|
|
574
|
+
ORDER BY created_seq
|
|
575
|
+
`).all(cutoff);
|
|
576
|
+
const markUncertain = this.db.prepare(`
|
|
577
|
+
UPDATE deliveries SET state='uncertain',reconciliation_pending=0,updated_at=?,finished_at=?,
|
|
578
|
+
last_error='reconciliation TTL expired without sufficient evidence'
|
|
579
|
+
WHERE delivery_id=? AND state='submission_started' AND reconciliation_pending=1
|
|
580
|
+
`);
|
|
581
|
+
for (const row of unresolved) {
|
|
582
|
+
if (markUncertain.run(now, now, row.delivery_id).changes !== 1)
|
|
583
|
+
continue;
|
|
584
|
+
this.db.prepare(`
|
|
585
|
+
UPDATE delivery_attempts SET state='uncertain',finished_at=?,evidence='reconciliation TTL expired without sufficient evidence'
|
|
586
|
+
WHERE delivery_id=? AND target_daemon_boot_id=? AND attempt_no=? AND state='begun'
|
|
587
|
+
`).run(now, row.delivery_id, row.target_daemon_boot_id, row.attempt_no);
|
|
588
|
+
this.insertFailureNotice(row, "uncertain", "reconciliation TTL expired without sufficient evidence", now);
|
|
589
|
+
changed++;
|
|
590
|
+
uncertain++;
|
|
591
|
+
}
|
|
592
|
+
return { changed, uncertain };
|
|
393
593
|
});
|
|
394
|
-
const
|
|
395
|
-
if (changed)
|
|
396
|
-
this.emit("expired", { count: changed });
|
|
397
|
-
return changed;
|
|
594
|
+
const result = transaction();
|
|
595
|
+
if (result.changed)
|
|
596
|
+
this.emit("expired", { count: result.changed, uncertain: result.uncertain });
|
|
597
|
+
return result.changed;
|
|
398
598
|
}
|
|
399
599
|
/** Next queued/retry expiry for bounded maintenance scheduling. */
|
|
400
600
|
nextExpiryAt() {
|
|
401
601
|
const row = this.db.prepare(`
|
|
402
|
-
SELECT MIN(created_at) AS created_at FROM deliveries
|
|
602
|
+
SELECT MIN(created_at) AS created_at FROM deliveries
|
|
603
|
+
WHERE state IN ('queued','retry_wait') OR (state='submission_started' AND reconciliation_pending=1)
|
|
403
604
|
`).get();
|
|
404
605
|
return row.created_at ? new Date(Date.parse(row.created_at) + DURABLE_DELIVERY_MAX_AGE_MS).toISOString() : null;
|
|
405
606
|
}
|
|
@@ -414,23 +615,17 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
414
615
|
recoverForBoot(managerBootId) {
|
|
415
616
|
const now = new Date().toISOString();
|
|
416
617
|
const transaction = this.db.transaction(() => {
|
|
417
|
-
const uncertainRows = this.db.prepare(`
|
|
418
|
-
SELECT * FROM deliveries WHERE manager_boot_id IS NOT NULL AND manager_boot_id<>? AND state='submission_started'
|
|
419
|
-
`).all(managerBootId);
|
|
420
618
|
const queued = this.db.prepare(`
|
|
421
619
|
UPDATE deliveries SET state='queued',manager_boot_id=NULL,target_daemon_boot_id=NULL,next_attempt_at=NULL,updated_at=?
|
|
422
620
|
WHERE manager_boot_id IS NOT NULL AND manager_boot_id<>? AND state IN ('delivering','retry_wait')
|
|
423
621
|
`).run(now, managerBootId).changes;
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
const
|
|
427
|
-
UPDATE deliveries SET
|
|
622
|
+
// Keep the submission fence and target lane while bounded pane/transcript
|
|
623
|
+
// evidence is captured. Never replay an in-flight row blindly.
|
|
624
|
+
const reconciliationPending = this.db.prepare(`
|
|
625
|
+
UPDATE deliveries SET reconciliation_pending=1,updated_at=?,last_error='process restarted during submission; reconciliation required'
|
|
428
626
|
WHERE manager_boot_id IS NOT NULL AND manager_boot_id<>? AND state='submission_started'
|
|
429
|
-
`).run(now,
|
|
430
|
-
|
|
431
|
-
this.insertFailureNotice(row, "uncertain", "process restarted during submission; reconciliation required", now);
|
|
432
|
-
}
|
|
433
|
-
return { queued, uncertain };
|
|
627
|
+
`).run(now, managerBootId).changes;
|
|
628
|
+
return { queued, reconciliationPending };
|
|
434
629
|
});
|
|
435
630
|
return transaction();
|
|
436
631
|
}
|
|
@@ -491,34 +686,101 @@ export class DeliveryOutbox extends EventEmitter {
|
|
|
491
686
|
recoverTargetGeneration(targetInstance, currentTargetBootId) {
|
|
492
687
|
const now = new Date().toISOString();
|
|
493
688
|
const transaction = this.db.transaction(() => {
|
|
494
|
-
const uncertainRows = this.db.prepare(`
|
|
495
|
-
SELECT * FROM deliveries WHERE target_instance=? AND target_daemon_boot_id IS NOT NULL
|
|
496
|
-
AND target_daemon_boot_id<>? AND state='submission_started'
|
|
497
|
-
`).all(targetInstance, currentTargetBootId);
|
|
498
689
|
const queued = this.db.prepare(`
|
|
499
690
|
UPDATE deliveries SET state='queued',manager_boot_id=NULL,target_daemon_boot_id=NULL,next_attempt_at=NULL,updated_at=?
|
|
500
691
|
WHERE target_instance=? AND target_daemon_boot_id IS NOT NULL AND target_daemon_boot_id<>?
|
|
501
692
|
AND state IN ('delivering','retry_wait')
|
|
502
693
|
`).run(now, targetInstance, currentTargetBootId).changes;
|
|
503
|
-
const
|
|
504
|
-
UPDATE deliveries SET
|
|
694
|
+
const reconciliationPending = this.db.prepare(`
|
|
695
|
+
UPDATE deliveries SET reconciliation_pending=1,updated_at=?,last_error='target daemon generation changed during submission; reconciliation required'
|
|
505
696
|
WHERE target_instance=? AND target_daemon_boot_id IS NOT NULL AND target_daemon_boot_id<>?
|
|
506
697
|
AND state='submission_started'
|
|
507
|
-
`).run(now,
|
|
508
|
-
|
|
509
|
-
this.insertFailureNotice(row, "uncertain", "target daemon generation changed during submission; reconciliation required", now);
|
|
510
|
-
}
|
|
511
|
-
return { queued, uncertain };
|
|
698
|
+
`).run(now, targetInstance, currentTargetBootId).changes;
|
|
699
|
+
return { queued, reconciliationPending };
|
|
512
700
|
});
|
|
513
701
|
const result = transaction();
|
|
514
|
-
if (result.queued + result.
|
|
702
|
+
if (result.queued + result.reconciliationPending > 0)
|
|
515
703
|
this.emit("generation_recovered", { targetInstance, ...result });
|
|
516
704
|
return result;
|
|
517
705
|
}
|
|
706
|
+
/** Fence in-flight writes before an in-process target stop begins. */
|
|
707
|
+
markTargetReconciliationPending(targetInstance, targetBootId) {
|
|
708
|
+
const now = new Date().toISOString();
|
|
709
|
+
return this.db.prepare(`
|
|
710
|
+
UPDATE deliveries SET reconciliation_pending=1,updated_at=?,last_error='target daemon stopping; reconciliation required'
|
|
711
|
+
WHERE target_instance=? AND target_daemon_boot_id=? AND state='submission_started'
|
|
712
|
+
`).run(now, targetInstance, targetBootId).changes;
|
|
713
|
+
}
|
|
714
|
+
getReconciliationCandidates(targetInstance) {
|
|
715
|
+
const rows = this.db.prepare(`
|
|
716
|
+
SELECT d.*, a.target_daemon_boot_id AS attempt_target_boot_id,
|
|
717
|
+
a.attempt_no AS attempt_attempt_no, a.backend AS attempt_backend,
|
|
718
|
+
a.window_id AS attempt_window_id, a.transcript_path AS attempt_transcript_path,
|
|
719
|
+
a.transcript_offset AS attempt_transcript_offset,
|
|
720
|
+
a.transcript_session_id AS attempt_transcript_session_id,
|
|
721
|
+
a.submission_mode AS attempt_submission_mode, a.backend_version AS attempt_backend_version,
|
|
722
|
+
a.queue_resume_policy AS attempt_queue_resume_policy,
|
|
723
|
+
a.enter_started_at AS attempt_enter_started_at
|
|
724
|
+
FROM deliveries d JOIN delivery_attempts a ON a.delivery_id=d.delivery_id
|
|
725
|
+
AND a.attempt_no=d.attempt_no AND a.target_daemon_boot_id=d.target_daemon_boot_id
|
|
726
|
+
WHERE d.target_instance=? AND d.state='submission_started' AND d.reconciliation_pending=1
|
|
727
|
+
ORDER BY d.created_seq
|
|
728
|
+
`).all(targetInstance);
|
|
729
|
+
return rows.map(row => ({
|
|
730
|
+
...mapRow(row),
|
|
731
|
+
attempt: {
|
|
732
|
+
targetDaemonBootId: row.attempt_target_boot_id,
|
|
733
|
+
attemptNo: row.attempt_attempt_no,
|
|
734
|
+
backend: row.attempt_backend,
|
|
735
|
+
backendVersion: row.attempt_backend_version,
|
|
736
|
+
windowId: row.attempt_window_id,
|
|
737
|
+
transcriptPath: row.attempt_transcript_path,
|
|
738
|
+
transcriptOffset: row.attempt_transcript_offset,
|
|
739
|
+
transcriptSessionId: row.attempt_transcript_session_id,
|
|
740
|
+
submissionMode: row.attempt_submission_mode,
|
|
741
|
+
queueResumePolicy: row.attempt_queue_resume_policy,
|
|
742
|
+
enterStartedAt: row.attempt_enter_started_at,
|
|
743
|
+
},
|
|
744
|
+
}));
|
|
745
|
+
}
|
|
746
|
+
/** Atomically settle one old attempt using only its boot/attempt fence. */
|
|
747
|
+
reconcileAttempt(deliveryId, targetBootId, attemptNo, outcome, evidence) {
|
|
748
|
+
const now = new Date().toISOString();
|
|
749
|
+
const transaction = this.db.transaction(() => {
|
|
750
|
+
const parent = this.db.prepare("SELECT * FROM deliveries WHERE delivery_id=?").get(deliveryId);
|
|
751
|
+
if (!parent || parent.state !== "submission_started" || parent.target_daemon_boot_id !== targetBootId
|
|
752
|
+
|| parent.attempt_no !== attemptNo || parent.reconciliation_pending !== 1)
|
|
753
|
+
return false;
|
|
754
|
+
const terminal = outcome !== "retry_wait";
|
|
755
|
+
const changed = this.db.prepare(`
|
|
756
|
+
UPDATE deliveries SET state=?,reconciliation_pending=0,manager_boot_id=?,
|
|
757
|
+
target_daemon_boot_id=?,updated_at=?,finished_at=?,last_error=?,next_attempt_at=?
|
|
758
|
+
WHERE delivery_id=? AND state='submission_started' AND reconciliation_pending=1
|
|
759
|
+
AND target_daemon_boot_id=? AND attempt_no=?
|
|
760
|
+
`).run(outcome, outcome === "retry_wait" ? null : parent.manager_boot_id, outcome === "retry_wait" ? null : targetBootId, now, terminal ? now : null, outcome === "delivered" ? null : evidence.slice(0, 300), outcome === "retry_wait" ? new Date(Date.now() + DURABLE_DELIVERY_ABORT_BACKOFF_MS).toISOString() : null, deliveryId, targetBootId, attemptNo).changes;
|
|
761
|
+
if (changed !== 1)
|
|
762
|
+
return false;
|
|
763
|
+
const attemptState = outcome === "retry_wait" ? "aborted" : outcome;
|
|
764
|
+
this.db.prepare(`
|
|
765
|
+
UPDATE delivery_attempts SET state=?,abort_at=?,finished_at=?,evidence=?
|
|
766
|
+
WHERE delivery_id=? AND target_daemon_boot_id=? AND attempt_no=? AND state='begun'
|
|
767
|
+
`).run(attemptState, outcome === "retry_wait" ? now : null, terminal ? now : null, evidence.slice(0, 300), deliveryId, targetBootId, attemptNo);
|
|
768
|
+
if (outcome === "uncertain")
|
|
769
|
+
this.insertFailureNotice(parent, "uncertain", evidence, now);
|
|
770
|
+
return true;
|
|
771
|
+
});
|
|
772
|
+
const changed = transaction();
|
|
773
|
+
if (changed)
|
|
774
|
+
this.emit("state", { deliveryId, state: outcome });
|
|
775
|
+
return changed;
|
|
776
|
+
}
|
|
518
777
|
getUnansweredAccepted(sourceInstance, currentBootId) {
|
|
519
778
|
const rows = this.db.prepare(`
|
|
520
779
|
SELECT * FROM deliveries
|
|
521
780
|
WHERE source_instance=? AND source_daemon_boot_id<>? AND response_delivered_at IS NULL
|
|
781
|
+
-- Scheduler admission has no waiting source-agent tool response; its
|
|
782
|
+
-- stable run key and durable row are the recovery/lookup contract.
|
|
783
|
+
AND kind NOT IN ('raw_paste')
|
|
522
784
|
ORDER BY created_seq
|
|
523
785
|
`).all(sourceInstance, currentBootId);
|
|
524
786
|
return rows.map(mapRow);
|