@zq-silk/yui 0.16.2 → 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/agent/agent.js +4 -9
- package/dist/agent/executionComponents.js +4 -4
- package/dist/agentRun/agentRun.js +8 -8
- package/dist/artifacts/managedGit.js +3 -56
- package/dist/brief/taskBrief.js +3 -3
- package/dist/cli/updateOrchestrator.js +2 -13
- package/dist/cli/updatePorts.js +36 -69
- package/dist/cli/upgradeCommand.js +4 -8
- package/dist/cli.js +4 -7
- package/dist/commands/controllerCommands.js +1 -1
- package/dist/commands/taskCommands.js +8 -1
- package/dist/commands/taskRoleRuntimeStatus.js +1 -1
- package/dist/context/runInputContract.js +1 -1
- package/dist/controller/agentRuntimeObserver.js +4 -4
- package/dist/controller/clientRuntime.js +19 -44
- package/dist/controller/controller.js +16 -4
- package/dist/controller/fileSchedulerStoreAdapter.js +6 -6
- package/dist/controller/globalInputDelivery.js +3 -1
- package/dist/controller/jobSupervisor.js +3 -3
- package/dist/controller/providerRetryDelivery.js +128 -118
- package/dist/controller/runtime.js +1 -1
- package/dist/controller/structuredProviderObservation.js +1 -1
- package/dist/coordination/workMailbox.js +4 -4
- package/dist/core/controllerIdentity.js +25 -0
- package/dist/core/controllerProcessIdentity.js +2 -2
- package/dist/core/controllerServer.js +4 -2
- package/dist/core/protocol.js +1 -1
- package/dist/domain/validation.js +6 -0
- package/dist/event/taskEvent.js +3 -3
- package/dist/execution/workItemExecution.js +2 -2
- package/dist/executor/agentExecutor.js +6 -6
- package/dist/executor/effectiveLaunch.js +3 -3
- package/dist/grant/capabilityGrant.js +2 -2
- package/dist/input/inputRequest.js +4 -4
- package/dist/integration/changeSet.js +3 -3
- package/dist/integration/integrationAttempt.js +3 -3
- package/dist/integration/integrationSourceApplication.js +1 -1
- package/dist/job/durableJob.js +1 -1
- package/dist/job/jobRunner.js +2 -2
- package/dist/message/message.js +8 -6
- package/dist/milestone/milestone.js +3 -3
- package/dist/profile/agentProfile.js +3 -3
- package/dist/release/runtimeRelease.js +9 -7
- package/dist/repository/project.js +3 -3
- package/dist/resources/liveReferences.js +1 -1
- package/dist/resources/sqliteResourceRegistry.js +2 -2
- package/dist/review/reviewRound.js +5 -5
- package/dist/role/role.js +7 -8
- package/dist/runtime/acpProtocol.js +2 -3
- package/dist/runtime/agentDriverObservation.js +1 -1
- package/dist/runtime/agentHost.js +3 -3
- package/dist/runtime/agentHostProtocol.js +2 -2
- package/dist/runtime/codexInteractiveHost.js +1 -1
- package/dist/runtime/launchBroker.js +1 -1
- package/dist/runtime/processExitObservation.js +1 -1
- package/dist/runtime/providerContinuationReconciliationService.js +84 -75
- package/dist/runtime/providerRuntimeIdentity.js +3 -3
- package/dist/runtime/runtimeCoherence.js +7 -3
- package/dist/runtime/runtimeObservation.js +3 -3
- package/dist/runtime/sessionOwnerIdentity.js +1 -1
- package/dist/runtime/taskRuntimeIsolation.js +3 -3
- package/dist/runtime/tmuxAdapters.js +3 -3
- package/dist/scheduler/taskWake.js +1 -1
- package/dist/storage/baselineSchema.js +606 -0
- package/dist/storage/homeLayout.js +5 -16
- package/dist/storage/recordValidation.js +16 -4
- package/dist/storage/sqliteSchema.js +106 -1741
- package/dist/storage/sqliteStore.js +16 -14
- package/dist/storage/storageSchema.js +8 -7
- package/dist/storage/storageVersions.js +23 -16
- package/dist/storage/taskStore.js +3 -29
- package/dist/storage/upgrade/upgradeOrchestrator.js +14 -102
- package/dist/task/task.js +15 -7
- package/dist/task/taskActivation.js +5 -4
- package/dist/telemetry/sqliteTelemetryStore.js +2 -2
- package/dist/verification/gateArtifact.js +5 -3
- package/dist/verification/verificationPlan.js +6 -7
- package/dist/workItem/workItem.js +6 -6
- package/dist/workspace/cleanupInspection.js +1 -9
- package/dist/worktree/managedWorkspace.js +3 -3
- package/docs/managed-turn-and-session-runtime.md +26 -14
- package/docs/managed-turn-and-session-runtime.zh-CN.md +18 -10
- package/docs/release-workflow.md +60 -300
- package/docs/release-workflow.zh-CN.md +46 -234
- package/docs/sqlite-control-plane-design.md +48 -289
- package/docs/sqlite-control-plane-design.zh-CN.md +37 -53
- package/docs/storage-baseline.md +139 -0
- package/docs/storage-baseline.zh-CN.md +111 -0
- package/docs/task-delivery.md +3 -4
- package/docs/task-delivery.zh-CN.md +3 -3
- package/docs/testing/verification-levels.md +40 -180
- package/docs/testing/verification-levels.zh-CN.md +27 -131
- package/i18n/README.zh-CN.md +5 -4
- package/package.json +1 -1
- package/dist/storage/migrations/agentFailureContext.js +0 -22
- package/dist/storage/migrations/agentRunContract.js +0 -159
- package/dist/storage/migrations/artifactsToGit.js +0 -338
- package/dist/storage/migrations/collapseWorktreeLayout.js +0 -963
- package/dist/storage/migrations/currentInputContract.js +0 -86
- package/dist/storage/migrations/currentRuntimeContract.js +0 -228
- package/dist/storage/migrations/historicalVerificationPlan.js +0 -35
- package/dist/storage/migrations/integrationContinuation.js +0 -105
- package/dist/storage/migrations/narrowAgentFailureContext.js +0 -65
- package/dist/storage/migrations/notificationOnlyWakes.js +0 -74
- package/dist/storage/migrations/removeRuntimeGeneration.js +0 -207
- package/dist/storage/migrations/submitIntent.js +0 -126
- package/dist/storage/migrations/unifyHomeLayout.js +0 -925
- package/dist/storage/migrations/verificationPlanV1.js +0 -162
- package/dist/storage/migrations/verificationPolicy.js +0 -74
- package/dist/storage/migrations/workItemHistory.js +0 -46
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
/** Storage 3 -> 4. Frozen migration; runtime code must not depend on this module. */
|
|
3
|
-
export const REMOVE_RUNTIME_GENERATION_SQL = `
|
|
4
|
-
-- data-transform: remove-runtime-generation-records-v1
|
|
5
|
-
DROP TRIGGER telemetry_ai;
|
|
6
|
-
DROP TRIGGER telemetry_au;
|
|
7
|
-
DROP INDEX idx_telemetry_turn;
|
|
8
|
-
ALTER TABLE telemetry RENAME TO telemetry_v3;
|
|
9
|
-
ALTER TABLE telemetry_aggregate RENAME TO telemetry_aggregate_v3;
|
|
10
|
-
CREATE TABLE telemetry (
|
|
11
|
-
task_id TEXT NOT NULL, role_name TEXT NOT NULL, turn_id TEXT NOT NULL,
|
|
12
|
-
progress_id TEXT NOT NULL, sequence INTEGER, payload TEXT NOT NULL,
|
|
13
|
-
received_at TEXT NOT NULL,
|
|
14
|
-
PRIMARY KEY (task_id, role_name, turn_id, progress_id)
|
|
15
|
-
) WITHOUT ROWID;
|
|
16
|
-
CREATE INDEX idx_telemetry_turn ON telemetry(task_id, turn_id);
|
|
17
|
-
INSERT INTO telemetry
|
|
18
|
-
SELECT task_id, role_name, turn_id, progress_id, sequence, payload, received_at
|
|
19
|
-
FROM (
|
|
20
|
-
SELECT *, ROW_NUMBER() OVER (
|
|
21
|
-
PARTITION BY task_id, role_name, turn_id, progress_id
|
|
22
|
-
ORDER BY received_at DESC, COALESCE(sequence, -1) DESC, generation DESC
|
|
23
|
-
) AS rank FROM telemetry_v3
|
|
24
|
-
) WHERE rank = 1;
|
|
25
|
-
CREATE TABLE telemetry_aggregate (
|
|
26
|
-
task_id TEXT NOT NULL, role_name TEXT NOT NULL, turn_id TEXT NOT NULL,
|
|
27
|
-
first_at TEXT NOT NULL, last_at TEXT NOT NULL, count INTEGER NOT NULL,
|
|
28
|
-
max_sequence INTEGER, error_count INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL,
|
|
29
|
-
PRIMARY KEY (task_id, role_name, turn_id)
|
|
30
|
-
) WITHOUT ROWID;
|
|
31
|
-
INSERT INTO telemetry_aggregate
|
|
32
|
-
SELECT task_id, role_name, turn_id, MIN(first_at), MAX(last_at), SUM(count),
|
|
33
|
-
MAX(max_sequence), SUM(error_count), MAX(updated_at)
|
|
34
|
-
FROM telemetry_aggregate_v3 GROUP BY task_id, role_name, turn_id;
|
|
35
|
-
DROP TABLE telemetry_v3;
|
|
36
|
-
DROP TABLE telemetry_aggregate_v3;
|
|
37
|
-
CREATE TRIGGER telemetry_ai AFTER INSERT ON telemetry
|
|
38
|
-
BEGIN
|
|
39
|
-
INSERT INTO telemetry_aggregate
|
|
40
|
-
(task_id, role_name, turn_id, first_at, last_at, count, max_sequence, error_count, updated_at)
|
|
41
|
-
VALUES (NEW.task_id, NEW.role_name, NEW.turn_id, NEW.received_at, NEW.received_at,
|
|
42
|
-
1, NEW.sequence,
|
|
43
|
-
CASE WHEN json_valid(NEW.payload)
|
|
44
|
-
AND (COALESCE(json_extract(NEW.payload, '$.error'), '') <> ''
|
|
45
|
-
OR COALESCE(json_extract(NEW.payload, '$.errorKind'), '') <> '')
|
|
46
|
-
THEN 1 ELSE 0 END, NEW.received_at)
|
|
47
|
-
ON CONFLICT(task_id, role_name, turn_id) DO UPDATE SET
|
|
48
|
-
first_at = MIN(telemetry_aggregate.first_at, excluded.first_at),
|
|
49
|
-
last_at = MAX(telemetry_aggregate.last_at, excluded.last_at),
|
|
50
|
-
count = telemetry_aggregate.count + 1,
|
|
51
|
-
max_sequence = CASE WHEN excluded.max_sequence IS NOT NULL
|
|
52
|
-
AND (telemetry_aggregate.max_sequence IS NULL OR excluded.max_sequence > telemetry_aggregate.max_sequence)
|
|
53
|
-
THEN excluded.max_sequence ELSE telemetry_aggregate.max_sequence END,
|
|
54
|
-
error_count = telemetry_aggregate.error_count + excluded.error_count,
|
|
55
|
-
updated_at = excluded.updated_at;
|
|
56
|
-
END;
|
|
57
|
-
CREATE TRIGGER telemetry_au AFTER UPDATE ON telemetry
|
|
58
|
-
BEGIN
|
|
59
|
-
UPDATE telemetry_aggregate SET
|
|
60
|
-
last_at = MAX(last_at, NEW.received_at),
|
|
61
|
-
max_sequence = CASE WHEN NEW.sequence IS NOT NULL
|
|
62
|
-
AND (max_sequence IS NULL OR NEW.sequence > max_sequence) THEN NEW.sequence ELSE max_sequence END,
|
|
63
|
-
updated_at = NEW.received_at
|
|
64
|
-
WHERE task_id = NEW.task_id AND role_name = NEW.role_name AND turn_id = NEW.turn_id;
|
|
65
|
-
END;
|
|
66
|
-
|
|
67
|
-
DROP INDEX idx_session_owners_task;
|
|
68
|
-
ALTER TABLE session_owners RENAME TO session_owners_v3;
|
|
69
|
-
CREATE TABLE session_owners (
|
|
70
|
-
process_key TEXT PRIMARY KEY, scope TEXT NOT NULL CHECK(scope IN ('task','global')),
|
|
71
|
-
task_id TEXT, role_name TEXT NOT NULL, agent_id TEXT NOT NULL, native_session_id TEXT,
|
|
72
|
-
provider_root_pid INTEGER, payload TEXT NOT NULL, recorded_at TEXT NOT NULL
|
|
73
|
-
);
|
|
74
|
-
INSERT INTO session_owners
|
|
75
|
-
SELECT process_key, scope, task_id, role_name, agent_id, native_session_id,
|
|
76
|
-
provider_root_pid, payload, recorded_at
|
|
77
|
-
FROM (
|
|
78
|
-
SELECT *,
|
|
79
|
-
CAST(provider_root_pid AS TEXT) || '-' || json_extract(payload, '$.providerRoot.startIdentity') AS process_key,
|
|
80
|
-
ROW_NUMBER() OVER (
|
|
81
|
-
PARTITION BY provider_root_pid, json_extract(payload, '$.providerRoot.startIdentity')
|
|
82
|
-
ORDER BY recorded_at DESC, launch_id DESC
|
|
83
|
-
) AS rank
|
|
84
|
-
FROM session_owners_v3
|
|
85
|
-
) WHERE rank = 1;
|
|
86
|
-
DROP TABLE session_owners_v3;
|
|
87
|
-
CREATE INDEX idx_session_owners_task ON session_owners(task_id, role_name);
|
|
88
|
-
|
|
89
|
-
DROP INDEX idx_runtime_session_cleanup_required;
|
|
90
|
-
ALTER TABLE runtime_session_candidates RENAME TO runtime_session_candidates_v3;
|
|
91
|
-
CREATE TABLE runtime_session_candidates (
|
|
92
|
-
scope TEXT NOT NULL CHECK(scope IN ('task','global')), task_id TEXT NOT NULL,
|
|
93
|
-
role_name TEXT NOT NULL, agent_id TEXT NOT NULL, adapter_id TEXT NOT NULL,
|
|
94
|
-
native_session_id TEXT NOT NULL, session_updated_at TEXT NOT NULL,
|
|
95
|
-
PRIMARY KEY(scope, task_id, role_name),
|
|
96
|
-
CHECK((scope = 'task' AND length(task_id) > 0) OR (scope = 'global' AND task_id = ''))
|
|
97
|
-
);
|
|
98
|
-
INSERT INTO runtime_session_candidates
|
|
99
|
-
SELECT scope, task_id, role_name, agent_id, adapter_id, native_session_id, session_updated_at
|
|
100
|
-
FROM runtime_session_candidates_v3;
|
|
101
|
-
DROP TABLE runtime_session_candidates_v3;
|
|
102
|
-
|
|
103
|
-
-- Retire only obsolete launch bookkeeping; preserve pending intent and cleanup requests.
|
|
104
|
-
UPDATE mailboxes SET processing = NULL
|
|
105
|
-
WHERE target_kind IN ('role-runtime','global-role-runtime')
|
|
106
|
-
AND json_extract(processing, '$.owner') = 'runtime-lifecycle'
|
|
107
|
-
AND json_extract(processing, '$.batch.reasons') = '["runtime-launch-reserved"]';
|
|
108
|
-
`;
|
|
109
|
-
export function removeRuntimeGenerationRecords(db) {
|
|
110
|
-
const object = (value) => (value !== null && typeof value === "object" && !Array.isArray(value));
|
|
111
|
-
const obsolete = new Set([
|
|
112
|
-
"runtimeGenerationId", "hostActivationId", "activationId", "continuationGeneration"
|
|
113
|
-
]);
|
|
114
|
-
// Accepted Jobs retain exactly the Session authority they already had.
|
|
115
|
-
// Never refresh a revoked/stale binding to the currently active Session.
|
|
116
|
-
for (const row of db.prepare("SELECT task_id, job_id, payload FROM durable_jobs").all()) {
|
|
117
|
-
const job = JSON.parse(row.payload);
|
|
118
|
-
if (!object(job.operation) || typeof job.operation.actorId !== "string")
|
|
119
|
-
continue;
|
|
120
|
-
const prefix = `task:${row.task_id}/role:`;
|
|
121
|
-
if (!job.operation.actorId.startsWith(prefix))
|
|
122
|
-
continue;
|
|
123
|
-
const roleName = job.operation.actorId.slice(prefix.length);
|
|
124
|
-
const roleRow = db.prepare("SELECT payload FROM task_roles WHERE task_id = ? AND role_name = ?")
|
|
125
|
-
.get(row.task_id, roleName);
|
|
126
|
-
const sessionsRow = db.prepare("SELECT payload FROM role_session_sets WHERE task_id = ? AND role_name = ?")
|
|
127
|
-
.get(row.task_id, roleName);
|
|
128
|
-
if (roleRow === undefined || sessionsRow === undefined)
|
|
129
|
-
continue;
|
|
130
|
-
const role = JSON.parse(roleRow.payload);
|
|
131
|
-
const sessions = JSON.parse(sessionsRow.payload);
|
|
132
|
-
const session = sessions.sessions?.[sessions.activeAgentId];
|
|
133
|
-
if (session?.status !== "active" || role.activeAgentId !== sessions.activeAgentId)
|
|
134
|
-
continue;
|
|
135
|
-
const key = db.prepare("SELECT hash FROM job_caller_key_hashes WHERE task_id = ? AND role_name = ? AND agent_id = ?")
|
|
136
|
-
.get(row.task_id, roleName, session.agentId);
|
|
137
|
-
if (key === undefined)
|
|
138
|
-
continue;
|
|
139
|
-
const identity = [session.agentId, session.adapterId, session.nativeSessionId];
|
|
140
|
-
const prior = createHash("sha256").update(JSON.stringify([key.hash, ...identity])).digest("hex");
|
|
141
|
-
if (job.operation.authorityRef !== prior)
|
|
142
|
-
continue;
|
|
143
|
-
job.operation.authorityRef = createHash("sha256").update(JSON.stringify(identity)).digest("hex");
|
|
144
|
-
db.prepare("UPDATE durable_jobs SET payload = ? WHERE task_id = ? AND job_id = ?")
|
|
145
|
-
.run(JSON.stringify(job), row.task_id, row.job_id);
|
|
146
|
-
}
|
|
147
|
-
db.exec("DROP TABLE job_caller_key_hashes");
|
|
148
|
-
const clean = (value) => {
|
|
149
|
-
if (Array.isArray(value))
|
|
150
|
-
return value.map(clean);
|
|
151
|
-
if (!object(value))
|
|
152
|
-
return value;
|
|
153
|
-
const next = {};
|
|
154
|
-
for (const [key, child] of Object.entries(value)) {
|
|
155
|
-
if (obsolete.has(key))
|
|
156
|
-
continue;
|
|
157
|
-
if (key === "activations" && typeof value.providerNamespace === "string"
|
|
158
|
-
&& Array.isArray(value.conversations))
|
|
159
|
-
continue;
|
|
160
|
-
if (key === "generation" && typeof value.continuationId === "string")
|
|
161
|
-
continue;
|
|
162
|
-
if (key === "generation" && value.kind === "yui-task-runtime-isolation")
|
|
163
|
-
continue;
|
|
164
|
-
next[key] = clean(child);
|
|
165
|
-
}
|
|
166
|
-
if (typeof next.providerNamespace === "string" && Array.isArray(next.conversations)
|
|
167
|
-
&& object(next.authority) && next.authority.owner === "controller") {
|
|
168
|
-
next.authority.holderId = "controller";
|
|
169
|
-
}
|
|
170
|
-
if (next.authorityOwner === "controller" && typeof next.holderId === "string") {
|
|
171
|
-
next.holderId = "controller";
|
|
172
|
-
}
|
|
173
|
-
if (next.kind === "yui-task-runtime-isolation" && object(next.roots)) {
|
|
174
|
-
next.roots.runtime = next.roots.generation;
|
|
175
|
-
delete next.roots.generation;
|
|
176
|
-
}
|
|
177
|
-
return next;
|
|
178
|
-
};
|
|
179
|
-
// These are system-owned record families, not arbitrary user documents or Job outputs.
|
|
180
|
-
for (const table of ["role_session_sets", "global_role_session_sets", "turns", "session_owners", "events"]) {
|
|
181
|
-
const columns = db.prepare(`PRAGMA table_info("${table}")`).all();
|
|
182
|
-
const keys = columns.filter((column) => column.pk > 0).sort((a, b) => a.pk - b.pk).map((c) => c.name);
|
|
183
|
-
const rows = db.prepare(`SELECT * FROM "${table}"`).all();
|
|
184
|
-
const where = keys.map((key) => `"${key}" = ?`).join(" AND ");
|
|
185
|
-
const update = db.prepare(`UPDATE "${table}" SET payload = ?${table === "events" ? ", type = ?" : ""} WHERE ${where}`);
|
|
186
|
-
for (const row of rows) {
|
|
187
|
-
const raw = JSON.parse(row.payload);
|
|
188
|
-
const migrated = clean(raw);
|
|
189
|
-
if (table === "events" && object(migrated.payload)
|
|
190
|
-
&& typeof migrated.payload.observation === "string"
|
|
191
|
-
&& typeof migrated.type === "string" && migrated.type.startsWith("runtime.")) {
|
|
192
|
-
const observation = JSON.parse(migrated.payload.observation);
|
|
193
|
-
const current = clean(observation);
|
|
194
|
-
if (typeof current.kind === "string" && current.kind.startsWith("activation.")) {
|
|
195
|
-
// Keep the historical fact, but it is no longer a current lifecycle event.
|
|
196
|
-
migrated.type = "runtime.historical-attachment-observed";
|
|
197
|
-
}
|
|
198
|
-
if (typeof current.stopReceiptId === "string") {
|
|
199
|
-
current.stopRequested = true;
|
|
200
|
-
delete current.stopReceiptId;
|
|
201
|
-
}
|
|
202
|
-
migrated.payload.observation = JSON.stringify(current);
|
|
203
|
-
}
|
|
204
|
-
update.run(JSON.stringify(migrated), ...(table === "events" ? [migrated.type] : []), ...keys.map((key) => row[key]));
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { createTaskEvent } from "../../event/taskEvent.js";
|
|
2
|
-
/**
|
|
3
|
-
* Storage 18 -> 19 data step for task-32 Requirement A (submit intent & Draft
|
|
4
|
-
* auto-routing).
|
|
5
|
-
*
|
|
6
|
-
* This helper is INDEPENDENT, deterministic and idempotent. The unified 18 -> 19
|
|
7
|
-
* migration owned by the artifacts WorkItem REGISTERS it (as its `migrateData`)
|
|
8
|
-
* and bumps `CURRENT_STORAGE_VERSION`; this module neither registers a second
|
|
9
|
-
* migration nor changes any schema.
|
|
10
|
-
*
|
|
11
|
-
* Requirement A adds three optional fields inside `messages.payload`, all part of
|
|
12
|
-
* this same 18 -> 19 contract even though each is optional — "optional" governs a
|
|
13
|
-
* single record's shape, not the Home compatibility line, so the version
|
|
14
|
-
* transition is still declared here and carried by the unified migration:
|
|
15
|
-
*
|
|
16
|
-
* - `intent` (record | discuss | develop): an absent value already reads as
|
|
17
|
-
* `discuss` (never `develop`), so historical Messages need no rewrite.
|
|
18
|
-
* - `submissionKey`: the client-chosen idempotency key. Only ever set going
|
|
19
|
-
* forward; NO historical key is fabricated (task-32 §2.3, message review), so
|
|
20
|
-
* old Messages stay keyless and non-idempotent exactly as before.
|
|
21
|
-
* - `submissionReceipt`: the frozen disposition a keyed submission replays from.
|
|
22
|
-
* Written only alongside a new `submissionKey`, so it too never appears on
|
|
23
|
-
* historical data.
|
|
24
|
-
*
|
|
25
|
-
* Because none of the three needs a value backfilled onto old rows, the only data
|
|
26
|
-
* step this migration performs is the planning-entered reconstruction below.
|
|
27
|
-
*
|
|
28
|
-
* The one fact old data cannot express is "this Draft already entered planning".
|
|
29
|
-
* Before Requirement A a Leader-waking user/operator submission on a Draft WAS
|
|
30
|
-
* the planning discussion, but it left no `task.planning-entered` fact because
|
|
31
|
-
* that event type did not exist. After the upgrade `develop` auto-activates an
|
|
32
|
-
* *unplanned* Draft; without this step a Draft that was mid-discussion would be
|
|
33
|
-
* mis-read as unplanned and snatched into auto-activation (task-32 §2.2, §4).
|
|
34
|
-
*
|
|
35
|
-
* So for every Draft that received at least one legitimate historical planning
|
|
36
|
-
* submission this appends exactly one `task.planning-entered` event, which makes
|
|
37
|
-
* {@link draftHasEnteredPlanning} derive `true` and routes a later develop to
|
|
38
|
-
* manual activation. A qualifying submission is an *unaddressed* user/operator
|
|
39
|
-
* Message that was not save-only (`wakePolicy !== "none"`) — byte-for-byte the
|
|
40
|
-
* same shape the new discuss path routes through the shared submission service.
|
|
41
|
-
*
|
|
42
|
-
* This is deliberately the conservative direction: it can only ever make develop
|
|
43
|
-
* MORE cautious, never activate a Draft it should not. Save-only Messages (old
|
|
44
|
-
* `wakePolicy === "none"`, the shape `record` now names) are excluded, so a Draft
|
|
45
|
-
* that only ever recorded context stays legitimately unplanned and its first
|
|
46
|
-
* develop may still auto-activate. Non-Draft Tasks are skipped because
|
|
47
|
-
* {@link draftHasEnteredPlanning} answers only about Drafts, and Activation
|
|
48
|
-
* requests are left untouched — a legacy request's absent `origin` is the
|
|
49
|
-
* truthful marker that it stays behind explicit activation.
|
|
50
|
-
*/
|
|
51
|
-
export function migrateSubmitIntent(db) {
|
|
52
|
-
const drafts = db.prepare("SELECT task_id FROM tasks_catalog WHERE status = 'draft'").all();
|
|
53
|
-
if (drafts.length === 0)
|
|
54
|
-
return;
|
|
55
|
-
const listMessages = db.prepare("SELECT payload FROM messages WHERE task_id = ? ORDER BY seq ASC");
|
|
56
|
-
const listEventRows = db.prepare("SELECT event_id, type FROM events WHERE task_id = ?");
|
|
57
|
-
const readHighWater = db.prepare("SELECT high_water FROM id_sequences WHERE task_id = ? AND kind = 'event'");
|
|
58
|
-
// Advance the per-Task event high-water to exactly the id we just consumed, so
|
|
59
|
-
// the runtime keeps allocating strictly above every migrated event (mirrors
|
|
60
|
-
// sqliteStore's id_sequences bookkeeping).
|
|
61
|
-
const upsertHighWater = db.prepare(`INSERT INTO id_sequences (task_id, kind, high_water) VALUES (?, 'event', ?)
|
|
62
|
-
ON CONFLICT(task_id, kind) DO UPDATE SET high_water = ?`);
|
|
63
|
-
const insertEvent = db.prepare("INSERT INTO events (task_id, event_id, type, occurred_at, payload) VALUES (?, ?, ?, ?, ?)");
|
|
64
|
-
for (const { task_id: taskId } of drafts) {
|
|
65
|
-
const eventRows = listEventRows.all(taskId);
|
|
66
|
-
// Idempotent: a Draft that already carries the fact (from a prior run of this
|
|
67
|
-
// migration, or from a real runtime planning entry) is left untouched.
|
|
68
|
-
if (eventRows.some((row) => row.type === PLANNING_ENTERED_EVENT_TYPE))
|
|
69
|
-
continue;
|
|
70
|
-
const entry = firstPlanningSubmission(listMessages.all(taskId));
|
|
71
|
-
if (entry === undefined)
|
|
72
|
-
continue;
|
|
73
|
-
// Allocate the next event id from the greater of the recorded high-water and
|
|
74
|
-
// the largest event id actually present, so a fixture or historical database
|
|
75
|
-
// whose id_sequences lags the events table can never produce a colliding id.
|
|
76
|
-
const highWater = readHighWater.get(taskId)?.high_water ?? 0;
|
|
77
|
-
const maxEventSeq = eventRows.reduce((max, row) => Math.max(max, eventSequence(row.event_id)), 0);
|
|
78
|
-
const seq = Math.max(highWater, maxEventSeq) + 1;
|
|
79
|
-
// Build through the shared constructor so a migrated event is byte-identical
|
|
80
|
-
// in shape to one the shared submission service writes at runtime, and dated
|
|
81
|
-
// to the historical submission rather than the wall-clock upgrade moment.
|
|
82
|
-
const event = createTaskEvent(`event-${seq}`, taskId, PLANNING_ENTERED_EVENT_TYPE, { messageId: entry.id, intent: "discuss", derivedBy: MIGRATION_PROVENANCE }, new Date(entry.createdAt));
|
|
83
|
-
insertEvent.run(taskId, event.id, event.type, event.createdAt, JSON.stringify(event));
|
|
84
|
-
upsertHighWater.run(taskId, seq, seq);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* The never-compacted event type recording a Draft entered planning. Inlined as a
|
|
89
|
-
* literal rather than imported so this released migration stays frozen against a
|
|
90
|
-
* future runtime rename of the constant; `TASK_PLANNING_ENTERED_EVENT` is the
|
|
91
|
-
* runtime owner, and a tripwire test asserts the two never diverge.
|
|
92
|
-
*/
|
|
93
|
-
const PLANNING_ENTERED_EVENT_TYPE = "task.planning-entered";
|
|
94
|
-
/** Marks the event as reconstructed by this migration, for later audit. */
|
|
95
|
-
const MIGRATION_PROVENANCE = "submit-intent-migration";
|
|
96
|
-
/**
|
|
97
|
-
* The earliest Message on a Draft that represents a historical planning
|
|
98
|
-
* submission, or `undefined` if the Draft only ever recorded save-only context.
|
|
99
|
-
*
|
|
100
|
-
* A qualifying Message is an unaddressed (`recipient === undefined`) user or
|
|
101
|
-
* operator Message whose wake policy was not `none`. An absent wake policy
|
|
102
|
-
* qualifies: before Requirement A that was the default Leader-waking submission,
|
|
103
|
-
* exactly the shape an omitted intent normalizes to `discuss` today.
|
|
104
|
-
*/
|
|
105
|
-
function firstPlanningSubmission(rows) {
|
|
106
|
-
for (const row of rows) {
|
|
107
|
-
const message = JSON.parse(row.payload);
|
|
108
|
-
if (message.kind !== "user" && message.kind !== "operator")
|
|
109
|
-
continue;
|
|
110
|
-
if (message.recipient !== undefined)
|
|
111
|
-
continue; // an addressed continuation, not a submission
|
|
112
|
-
if (message.wakePolicy === "none")
|
|
113
|
-
continue; // old save-only == record, never planning
|
|
114
|
-
if (typeof message.id !== "string")
|
|
115
|
-
continue;
|
|
116
|
-
if (typeof message.createdAt !== "string")
|
|
117
|
-
continue;
|
|
118
|
-
return { id: message.id, createdAt: message.createdAt };
|
|
119
|
-
}
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
122
|
-
/** The numeric suffix of an `event-<n>` id, or 0 for any unexpected shape. */
|
|
123
|
-
function eventSequence(eventId) {
|
|
124
|
-
const match = /^event-(\d+)$/.exec(eventId);
|
|
125
|
-
return match === null ? 0 : Number.parseInt(match[1], 10);
|
|
126
|
-
}
|