@songsid/agend 2.3.2-beta.9 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/README.zh-TW.md +6 -0
- package/dist/agent-cli.js +48 -0
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +6 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.js +3 -0
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/claude-code.js +23 -1
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-session-lookup.d.ts +15 -0
- package/dist/backend/codex-session-lookup.js +58 -0
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.js +15 -4
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro-classic-archive.d.ts +57 -0
- package/dist/backend/kiro-classic-archive.js +622 -0
- package/dist/backend/kiro-classic-archive.js.map +1 -0
- package/dist/backend/kiro-classic-links.d.ts +24 -0
- package/dist/backend/kiro-classic-links.js +135 -0
- package/dist/backend/kiro-classic-links.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +100 -1
- package/dist/backend/kiro-identity.js +262 -31
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro-migration-verify.d.ts +10 -0
- package/dist/backend/kiro-migration-verify.js +52 -0
- package/dist/backend/kiro-migration-verify.js.map +1 -0
- package/dist/backend/kiro-reconcile-store.d.ts +27 -0
- package/dist/backend/kiro-reconcile-store.js +344 -0
- package/dist/backend/kiro-reconcile-store.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.js +35 -1
- package/dist/backend/kiro-v2-store-worker.js.map +1 -1
- package/dist/backend/kiro-v2-store.d.ts +31 -1
- package/dist/backend/kiro-v2-store.js +22 -12
- package/dist/backend/kiro-v2-store.js.map +1 -1
- package/dist/backend/kiro.d.ts +124 -1
- package/dist/backend/kiro.js +553 -29
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +102 -0
- package/dist/backend/types.js +9 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/cache-service.d.ts +13 -1
- package/dist/cache-service.js +45 -2
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/ipc-timeouts.js +3 -0
- package/dist/channel/ipc-timeouts.js.map +1 -1
- package/dist/channel/mcp-tools.js +40 -5
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.js +2 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +6 -0
- package/dist/classic-channel-manager.js +25 -0
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +50 -4
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.d.ts +7 -1
- package/dist/config-validator.js +17 -8
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +66 -0
- package/dist/daemon.js +421 -19
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +15 -4
- package/dist/dashboard-permissions.js +49 -23
- package/dist/dashboard-permissions.js.map +1 -1
- package/dist/doctor.js +2 -1
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-control-audit.d.ts +1 -1
- package/dist/fleet-control-audit.js +3 -0
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-manager.d.ts +90 -3
- package/dist/fleet-manager.js +569 -41
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +9 -0
- package/dist/forged-envelope-scan.js +30 -5
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-instance.d.ts +24 -0
- package/dist/general-instance.js +53 -0
- package/dist/general-instance.js.map +1 -1
- package/dist/general-knowledge/skills/docker-install/SKILL.md +25 -0
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -3
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +34 -0
- package/dist/general-knowledge/skills/session-management/SKILL.md +1 -1
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +32 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +32 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +57 -17
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +57 -17
- package/dist/instance-config-impact.js +3 -2
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +4 -0
- package/dist/instance-lifecycle.js +7 -0
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/kiro-migrate.d.ts +19 -2
- package/dist/kiro-migrate.js +133 -35
- package/dist/kiro-migrate.js.map +1 -1
- package/dist/kiro-reconcile.d.ts +21 -0
- package/dist/kiro-reconcile.js +88 -0
- package/dist/kiro-reconcile.js.map +1 -0
- package/dist/kiro-session-proof.d.ts +41 -0
- package/dist/kiro-session-proof.js +89 -0
- package/dist/kiro-session-proof.js.map +1 -0
- package/dist/kiro-tui-forensic.d.ts +19 -0
- package/dist/kiro-tui-forensic.js +124 -0
- package/dist/kiro-tui-forensic.js.map +1 -0
- package/dist/locale.js +198 -0
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +2 -2
- package/dist/log-service.js.map +1 -1
- package/dist/needs-you.d.ts +3 -1
- package/dist/needs-you.js +4 -2
- package/dist/needs-you.js.map +1 -1
- package/dist/office-levels.d.ts +64 -0
- package/dist/office-levels.js +66 -0
- package/dist/office-levels.js.map +1 -0
- package/dist/office-share-page.d.ts +3 -1
- package/dist/office-share-page.js +8 -3
- package/dist/office-share-page.js.map +1 -1
- package/dist/office-share-service.d.ts +6 -3
- package/dist/office-share-service.js +12 -8
- package/dist/office-share-service.js.map +1 -1
- package/dist/office-share.d.ts +45 -8
- package/dist/office-share.js +75 -15
- package/dist/office-share.js.map +1 -1
- package/dist/outbound-handlers.js +20 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +47 -2
- package/dist/outbound-schemas.js +41 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +1 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.d.ts +39 -1
- package/dist/quickstart-api.js +115 -17
- package/dist/quickstart-api.js.map +1 -1
- package/dist/remote-card-answer.d.ts +83 -0
- package/dist/remote-card-answer.js +208 -0
- package/dist/remote-card-answer.js.map +1 -0
- package/dist/remote-card-prompt.d.ts +38 -0
- package/dist/remote-card-prompt.js +86 -0
- package/dist/remote-card-prompt.js.map +1 -0
- package/dist/scheduler/task-board.d.ts +28 -6
- package/dist/scheduler/task-board.js +66 -11
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/service-installer.d.ts +13 -0
- package/dist/service-installer.js +15 -0
- package/dist/service-installer.js.map +1 -1
- package/dist/service-owner.d.ts +25 -0
- package/dist/service-owner.js +43 -2
- package/dist/service-owner.js.map +1 -1
- package/dist/settings-api.d.ts +2 -0
- package/dist/settings-api.js +18 -12
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +10 -5
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +7 -4
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-instance-patch.js +1 -1
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/setup-form.d.ts +2 -2
- package/dist/setup-form.js +139 -93
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-i18n.d.ts +25 -0
- package/dist/setup-i18n.js +226 -0
- package/dist/setup-i18n.js.map +1 -0
- package/dist/task-board-waker.js +7 -2
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tool-permissions.js +18 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/transcript-sources.d.ts +1 -0
- package/dist/transcript-sources.js +20 -3
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/ui/backend-signin.js +4 -3
- package/dist/ui/chat-store.js +3 -2
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/fleet-office-card.js +114 -0
- package/dist/ui/fleet-office-editor.js +81 -9
- package/dist/ui/fleet-office-layout.js +56 -11
- package/dist/ui/fleet-office-levels.js +105 -0
- package/dist/ui/fleet-office-model.js +9 -1
- package/dist/ui/fleet-office-pack.d.ts +15 -3
- package/dist/ui/fleet-office-pack.js +177 -23
- package/dist/ui/fleet-office-skin.js +39 -3
- package/dist/ui/fleet-office-store.js +74 -3
- package/dist/ui/fleet-office-view.js +8 -0
- package/dist/ui/fleet-office-zones.js +102 -19
- package/dist/ui/fleet-office.js +308 -44
- package/dist/ui/fleet-org.js +2 -0
- package/dist/ui/fleet-task-card.js +2 -2
- package/dist/ui/fleet-task-common.js +30 -7
- package/dist/ui/fleet-task-work.js +2 -2
- package/dist/ui/fleet-tasks.js +10 -11
- package/dist/ui/office-share-dialog.js +31 -7
- package/dist/ui/office-share-page.js +1 -0
- package/dist/ui/panel-chat.js +14 -2
- package/dist/ui/panel-log.js +4 -4
- package/dist/ui/panel-needs.js +1 -1
- package/dist/ui/panel-settings.js +16 -8
- package/dist/ui/settings-discord.js +3 -2
- package/dist/ui/settings-guests.js +17 -7
- package/dist/ui/settings-model.js +17 -9
- package/dist/ui/settings-registry.js +1 -0
- package/dist/ui/settings-strings.js +16 -4
- package/dist/ui/settings-token.js +3 -2
- package/dist/ui/settings-wizard.js +25 -9
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-stream.js +1 -1
- package/dist/ui/shared/app.css +70 -2
- package/dist/ui/shared/setup-host-text.js +107 -0
- package/dist/ui/structured-signin.js +89 -38
- package/dist/usage-rollup.d.ts +34 -3
- package/dist/usage-rollup.js +56 -3
- package/dist/usage-rollup.js.map +1 -1
- package/dist/web-api.d.ts +10 -1
- package/dist/web-api.js +207 -17
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +1 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-office-acts.d.ts +5 -2
- package/dist/web-office-acts.js +9 -4
- package/dist/web-office-acts.js.map +1 -1
- package/dist/web-office-pack-read.d.ts +35 -0
- package/dist/web-office-pack-read.js +252 -0
- package/dist/web-office-pack-read.js.map +1 -0
- package/dist/web-office-pack-tools.d.ts +241 -0
- package/dist/web-office-pack-tools.js +785 -0
- package/dist/web-office-pack-tools.js.map +1 -0
- package/dist/web-office-skins.d.ts +68 -4
- package/dist/web-office-skins.js +324 -33
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-office-store.d.ts +47 -4
- package/dist/web-office-store.js +124 -23
- package/dist/web-office-store.js.map +1 -1
- package/dist/web-settings-patch.d.ts +3 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +2 -2
- package/templates/windows-shortcuts.ps1 +18 -17
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
/** #2598: preserve a frozen classic copy outside Kiro's merged session index.
|
|
2
|
+
* Only an owned, complete carried V2 copy permits this. Never called while its CLI is alive.
|
|
3
|
+
* Production launch preparation runs this work on the existing bounded V2 worker lane.
|
|
4
|
+
*/
|
|
5
|
+
import { isDeepStrictEqual } from "node:util";
|
|
6
|
+
import Database from "better-sqlite3";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
+
import { closeSync, constants, existsSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
11
|
+
import { kiroAgentName, kiroFleetTag } from "./kiro-agent.js";
|
|
12
|
+
import { kiroCanonicalDirectory, kiroDirectoryKeys, kiroCarryCandidate, reinstateKiroCarryPin } from "./kiro-identity.js";
|
|
13
|
+
import { KiroTuiSnapshotSizeError, readKiroTuiAssistantTexts, snapshotKiroTuiFile } from "../kiro-tui-forensic.js";
|
|
14
|
+
import { archivedKiroAlternative, assertKiroReconciliationLaunch, inspectKiroReconciliation, inspectKiroReconciliationRollback, preserveKiroReconciliation, completeKiroSelection, finishKiroReconciliationLaunch, beginKiroReconciliationRollback, rollbackKiroSelection } from "./kiro-reconcile-store.js";
|
|
15
|
+
const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
16
|
+
const MAX_ROW_BYTES = 32 * 1024 * 1024;
|
|
17
|
+
const MAX_ARCHIVE_BYTES = 96 * 1024 * 1024;
|
|
18
|
+
const safeId = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,199}$/;
|
|
19
|
+
/** Full process listing is bounded; only the matching agent/id is inspected, no command lines are logged. */
|
|
20
|
+
export function assertKiroConversationStopped(input, listing, ownerOf) {
|
|
21
|
+
const agent = kiroAgentName(input.instance, kiroFleetTag(input.agendHome));
|
|
22
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23
|
+
const target = new RegExp(`(?:^|\\s)--(?:agent(?:=|\\s+)['"]?${escape(agent)}|resume-id(?:=|\\s+)['"]?${escape(input.sessionId)})['"]?(?=\\s|$)`);
|
|
24
|
+
const processes = listing ?? execFileSync("ps", ["-axo", "pid=,command="], { encoding: "utf8", timeout: 2000, maxBuffer: 4 * 1024 * 1024 });
|
|
25
|
+
if (!processes.trim())
|
|
26
|
+
throw new Error("Cannot prove the Kiro process is stopped: process listing is empty");
|
|
27
|
+
let parsed = 0;
|
|
28
|
+
for (const line of processes.split("\n")) {
|
|
29
|
+
const row = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
30
|
+
if (row)
|
|
31
|
+
parsed++;
|
|
32
|
+
if (row && target.test(row[2]))
|
|
33
|
+
throw new Error("The instance's Kiro process is still alive; stop it before repairing its stores");
|
|
34
|
+
if (row && !/--(?:agent|resume-id)(?:=|\s)/.test(row[2]) && /(?:kiro-cli(?:-chat)?(?:\s|$)|[/\\]tui\.js(?:\s|$))/.test(row[2])) {
|
|
35
|
+
const owner = ownerOf ? ownerOf(Number(row[1])) : listing !== undefined ? null : kiroProcessOwner(Number(row[1]), input.workingDirectory);
|
|
36
|
+
if (owner === input.instance || owner === null)
|
|
37
|
+
throw new Error("A Kiro process may still own this instance's conversation; stopped state is not proven");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (!parsed)
|
|
41
|
+
throw new Error("Cannot prove the Kiro process is stopped: process listing is invalid");
|
|
42
|
+
}
|
|
43
|
+
/** Catch surviving TUI/agent children whose argv omits --agent; never log process environments. */
|
|
44
|
+
function kiroProcessOwner(pid, workingDirectory) {
|
|
45
|
+
try {
|
|
46
|
+
if (process.platform === "linux") {
|
|
47
|
+
const root = `/proc/${pid}`;
|
|
48
|
+
const state = readFileSync(join(root, "stat"), "utf8");
|
|
49
|
+
if (/^[ZX] /.test(state.slice(state.lastIndexOf(")") + 2)))
|
|
50
|
+
return "gone";
|
|
51
|
+
const env = readFileSync(join(root, "environ"));
|
|
52
|
+
if (env.length > 4 * 1024 * 1024)
|
|
53
|
+
throw new Error("Kiro process environment is unobservable");
|
|
54
|
+
const marker = Buffer.from("AGEND_INSTANCE_NAME=");
|
|
55
|
+
const at = env.indexOf(marker);
|
|
56
|
+
if (at >= 0 && (at === 0 || env[at - 1] === 0)) {
|
|
57
|
+
const end = env.indexOf(0, at);
|
|
58
|
+
return env.subarray(at + marker.length, end < 0 ? env.length : end).toString("utf8") || null;
|
|
59
|
+
}
|
|
60
|
+
return kiroCanonicalDirectory(readlinkSync(join(root, "cwd"))) === kiroCanonicalDirectory(workingDirectory) ? null : "other-workspace";
|
|
61
|
+
}
|
|
62
|
+
const env = execFileSync("ps", ["eww", "-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: 2000, maxBuffer: 4 * 1024 * 1024 });
|
|
63
|
+
return /(?:^|\s)AGEND_INSTANCE_NAME=([^\s]+)/.exec(env)?.[1] ?? null;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (err.code === "ENOENT")
|
|
67
|
+
return "gone";
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function ownedFile(path) {
|
|
72
|
+
const st = lstatSync(path);
|
|
73
|
+
if (!st.isFile() || (process.getuid && st.uid !== process.getuid()))
|
|
74
|
+
throw new Error("Store or archive is not an owned real file");
|
|
75
|
+
}
|
|
76
|
+
function privateDirectory(path) {
|
|
77
|
+
const existed = existsSync(path);
|
|
78
|
+
if (!existed)
|
|
79
|
+
mkdirSync(path, { mode: 0o700 });
|
|
80
|
+
const st = lstatSync(path);
|
|
81
|
+
if (!st.isDirectory() || (process.getuid && st.uid !== process.getuid()) || (st.mode & 0o077))
|
|
82
|
+
throw new Error("Archive directory is not private and owned");
|
|
83
|
+
{
|
|
84
|
+
// Repeat on retry too: a previous failed sync may have left a visible but non-durable directory.
|
|
85
|
+
// Persist the directory itself AND its name in the parent before SQLite can remove the source.
|
|
86
|
+
for (const dir of [path, resolve(path, "..")]) {
|
|
87
|
+
const fd = openSync(dir, constants.O_RDONLY);
|
|
88
|
+
try {
|
|
89
|
+
fsyncSync(fd);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
closeSync(fd);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function readArchive(path) {
|
|
98
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
99
|
+
try {
|
|
100
|
+
const st = fstatSync(fd);
|
|
101
|
+
if (!st.isFile() || st.size > MAX_ARCHIVE_BYTES || (st.mode & 0o077) || (process.getuid && st.uid !== process.getuid()))
|
|
102
|
+
throw new Error("Archive is not a bounded private owned file");
|
|
103
|
+
const value = JSON.parse(readFileSync(fd, "utf8"));
|
|
104
|
+
if (!value?.payload || value.payload.version !== 1 || value.sha256 !== hash(JSON.stringify(value.payload)))
|
|
105
|
+
throw new Error("Archive checksum or version is invalid");
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
closeSync(fd);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function writeArchive(input, rows) {
|
|
113
|
+
const payload = { version: 1, instance: input.instance, directory: kiroCanonicalDirectory(input.workingDirectory), profile: input.credentialProfile,
|
|
114
|
+
sessionId: input.sessionId, dbPath: resolve(input.dbPath), rows };
|
|
115
|
+
const archive = { payload, sha256: hash(JSON.stringify(payload)) };
|
|
116
|
+
const base = join(input.agendHome, "kiro-classic-archives");
|
|
117
|
+
privateDirectory(base);
|
|
118
|
+
const root = join(base, hash(input.instance).slice(0, 32));
|
|
119
|
+
privateDirectory(root);
|
|
120
|
+
const path = join(root, `${input.sessionId}-${archive.sha256}.json`), temp = `${path}.${randomUUID()}.tmp`;
|
|
121
|
+
const fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
|
|
122
|
+
try {
|
|
123
|
+
writeFileSync(fd, JSON.stringify(archive) + "\n");
|
|
124
|
+
fsyncSync(fd);
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
closeSync(fd);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
linkSync(temp, path);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
if (err.code !== "EEXIST")
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
unlinkSync(temp);
|
|
138
|
+
}
|
|
139
|
+
const dir = openSync(root, constants.O_RDONLY);
|
|
140
|
+
try {
|
|
141
|
+
fsyncSync(dir);
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
closeSync(dir);
|
|
145
|
+
}
|
|
146
|
+
if (JSON.stringify(readArchive(path)) !== JSON.stringify(archive))
|
|
147
|
+
throw new Error("Archive read-back does not match the frozen rows");
|
|
148
|
+
return path;
|
|
149
|
+
}
|
|
150
|
+
function identity(input, engine) {
|
|
151
|
+
return { agendHome: input.agendHome, instance: input.instance, engine, workingDirectory: input.workingDirectory, credentialProfile: input.credentialProfile };
|
|
152
|
+
}
|
|
153
|
+
function proveOwnedCarry(input, allowAbandoned = false) {
|
|
154
|
+
const current = () => {
|
|
155
|
+
const candidate = kiroCarryCandidate(input);
|
|
156
|
+
return !!candidate && candidate.id === input.sessionId && (allowAbandoned || !candidate.repair);
|
|
157
|
+
};
|
|
158
|
+
if (!current())
|
|
159
|
+
throw new Error("Both classic and V2 must still be owned by this instance");
|
|
160
|
+
const metadata = snapshotKiroTuiFile(join(input.sessionsDir, `${input.sessionId}.json`), 8 * 1024 * 1024);
|
|
161
|
+
const meta = JSON.parse(metadata.text), state = meta.session_state;
|
|
162
|
+
if (meta.exported_from_v1 !== true || typeof meta.cwd !== "string" || kiroCanonicalDirectory(meta.cwd) !== kiroCanonicalDirectory(input.workingDirectory)
|
|
163
|
+
|| !state || state.version !== "v1" || !state.conversation_metadata || state.rts_model_state?.conversation_id !== input.sessionId)
|
|
164
|
+
throw new Error("No complete matching carried V2 copy is proven");
|
|
165
|
+
const read = readKiroTuiAssistantTexts({ sessionId: input.sessionId, sessionsDir: input.sessionsDir, stillOwned: current });
|
|
166
|
+
if (read.kind !== "ok" || !metadata.unchanged())
|
|
167
|
+
throw new Error("The carried V2 transcript is incomplete, changed or unreadable");
|
|
168
|
+
proveClassicHistoryCoverage(input);
|
|
169
|
+
}
|
|
170
|
+
/** Kiro 2.34 preserves each classic assistant message_id and converts every history pair into two JSONL records.
|
|
171
|
+
* Match the entire frozen prefix (user text, assistant text/tool inputs and tool outputs), not just readable syntax.
|
|
172
|
+
* Unknown classic formats fail closed; original non-message metadata remains in the exact restorable archive.
|
|
173
|
+
*/
|
|
174
|
+
export function proveClassicHistoryCoverage(input) {
|
|
175
|
+
ownedFile(input.dbPath);
|
|
176
|
+
const db = new Database(input.dbPath, { readonly: true, fileMustExist: true, timeout: 200 });
|
|
177
|
+
let values;
|
|
178
|
+
try {
|
|
179
|
+
const keys = kiroDirectoryKeys(input.workingDirectory), where = `key IN (${keys.map(() => "?").join(",")}) AND conversation_id=?`;
|
|
180
|
+
const sizes = db.prepare(`SELECT length(CAST(value AS BLOB)) AS n FROM conversations_v2 WHERE ${where}`).all(...keys, input.sessionId);
|
|
181
|
+
if (sizes.reduce((n, r) => n + r.n, 0) > MAX_ROW_BYTES)
|
|
182
|
+
throw new Error("Classic history exceeds the coverage limit");
|
|
183
|
+
values = db.prepare(`SELECT CAST(value AS BLOB) AS value FROM conversations_v2 WHERE ${where}`).all(...keys, input.sessionId).map(r => r.value);
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
db.close();
|
|
187
|
+
}
|
|
188
|
+
if (!values.length) {
|
|
189
|
+
const root = join(input.agendHome, "kiro-classic-archives", hash(input.instance).slice(0, 32));
|
|
190
|
+
for (const name of readdirSync(root)) {
|
|
191
|
+
if (!name.startsWith(input.sessionId + "-") || !name.endsWith(".json"))
|
|
192
|
+
continue;
|
|
193
|
+
const archive = readArchive(join(root, name)).payload;
|
|
194
|
+
if (archive.instance === input.instance && archive.sessionId === input.sessionId && archive.directory === kiroCanonicalDirectory(input.workingDirectory)
|
|
195
|
+
&& archive.profile === input.credentialProfile && archive.dbPath === resolve(input.dbPath))
|
|
196
|
+
values.push(...archive.rows.map(r => Buffer.from(r.valueBase64, "base64")));
|
|
197
|
+
}
|
|
198
|
+
if (!values.length)
|
|
199
|
+
throw new Error("No frozen source or verified archive proves complete history coverage");
|
|
200
|
+
}
|
|
201
|
+
const log = snapshotKiroTuiFile(join(input.sessionsDir, input.sessionId + ".jsonl"), 64 * 1024 * 1024);
|
|
202
|
+
const records = log.text.split("\n").filter(line => line.trim()).map(line => JSON.parse(line));
|
|
203
|
+
const fail = () => { throw new Error("The carried V2 copy does not cover the complete frozen classic history"); };
|
|
204
|
+
for (const bytes of values) {
|
|
205
|
+
const classic = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
206
|
+
if (classic.conversation_id !== input.sessionId || !Array.isArray(classic.history) || !classic.history.length || classic.next_message != null)
|
|
207
|
+
fail();
|
|
208
|
+
for (let i = 0; i < classic.history.length; i++) {
|
|
209
|
+
const turn = classic.history[i], user = turn?.user?.content, assistant = turn?.assistant;
|
|
210
|
+
const u = records[i * 2], a = records[i * 2 + 1];
|
|
211
|
+
const response = assistant?.Response, tool = assistant?.ToolUse, body = response ?? tool;
|
|
212
|
+
if (!body || response && tool || !body.message_id || a?.kind !== "AssistantMessage" || a.data?.message_id !== body.message_id || !Array.isArray(a.data.content))
|
|
213
|
+
fail();
|
|
214
|
+
const expected = [];
|
|
215
|
+
if (typeof body.content !== "string")
|
|
216
|
+
fail();
|
|
217
|
+
if (body.content || response)
|
|
218
|
+
expected.push({ kind: "text", data: body.content });
|
|
219
|
+
if (tool) {
|
|
220
|
+
if (!Array.isArray(tool.tool_uses))
|
|
221
|
+
fail();
|
|
222
|
+
for (const call of tool.tool_uses) {
|
|
223
|
+
if (!call || typeof call.id !== "string" || typeof call.name !== "string" || !call.args || typeof call.args !== "object")
|
|
224
|
+
fail();
|
|
225
|
+
expected.push({ kind: "toolUse", data: { toolUseId: call.id, name: call.name, input: call.args } });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Tool blocks may carry extra upstream metadata; compare the message-bearing fields exactly.
|
|
229
|
+
const actual = a.data.content.map((b) => b.kind === "toolUse" ? { kind: b.kind, data: { toolUseId: b.data?.toolUseId, name: b.data?.name, input: b.data?.input } } : b);
|
|
230
|
+
if (!isDeepStrictEqual(actual, expected) || !u?.data || (turn.user.images != null && (!Array.isArray(turn.user.images) || turn.user.images.length)))
|
|
231
|
+
fail();
|
|
232
|
+
if (user?.Prompt && typeof user.Prompt.prompt === "string") {
|
|
233
|
+
if (u.kind !== "Prompt" || !isDeepStrictEqual(u.data.content, [{ kind: "text", data: user.Prompt.prompt }]))
|
|
234
|
+
fail();
|
|
235
|
+
}
|
|
236
|
+
else if (Array.isArray((user?.ToolUseResults ?? user?.CancelledToolUses)?.tool_use_results)) {
|
|
237
|
+
if (u.kind !== "ToolResults" || !u.data.results || typeof u.data.results !== "object")
|
|
238
|
+
fail();
|
|
239
|
+
const cancelled = user?.CancelledToolUses;
|
|
240
|
+
const results = (user.ToolUseResults ?? cancelled).tool_use_results;
|
|
241
|
+
if (cancelled && (typeof cancelled.prompt !== "string" || !Array.isArray(u.data.content)
|
|
242
|
+
|| !u.data.content.some((b) => b.kind === "text" && b.data === cancelled.prompt)))
|
|
243
|
+
fail();
|
|
244
|
+
if (Object.keys(u.data.results).length !== results.length)
|
|
245
|
+
fail();
|
|
246
|
+
for (const result of results) {
|
|
247
|
+
let converted;
|
|
248
|
+
if (cancelled)
|
|
249
|
+
converted = "Cancelled";
|
|
250
|
+
else if (result.status === "Success")
|
|
251
|
+
converted = { Success: { items: result.content } };
|
|
252
|
+
else if (result.status === "Error" && Array.isArray(result.content) && result.content.every((x) => typeof x.Text === "string"))
|
|
253
|
+
converted = { Error: { Custom: result.content.map((x) => x.Text).join("\n") } };
|
|
254
|
+
else
|
|
255
|
+
fail();
|
|
256
|
+
if (!isDeepStrictEqual(u.data.results[result.tool_use_id]?.result, converted))
|
|
257
|
+
fail();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else
|
|
261
|
+
fail();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (!log.unchanged())
|
|
265
|
+
fail();
|
|
266
|
+
}
|
|
267
|
+
/** Unknown cwd is safe to ignore only for stable, owned files older than the carry, with no possible replacement work. */
|
|
268
|
+
function oldUnknownMetadata(input, name, created) {
|
|
269
|
+
const path = join(input.sessionsDir, name), logPath = join(input.sessionsDir, name.slice(0, -5) + ".jsonl");
|
|
270
|
+
try {
|
|
271
|
+
const before = lstatSync(path);
|
|
272
|
+
const old = (st) => st.isFile() && (!process.getuid || st.uid === process.getuid())
|
|
273
|
+
&& Number.isFinite(st.mtimeMs) && Number.isFinite(st.ctimeMs) && st.mtimeMs < created && st.ctimeMs < created;
|
|
274
|
+
if (!old(before))
|
|
275
|
+
return null;
|
|
276
|
+
const log = () => { try {
|
|
277
|
+
return lstatSync(logPath);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
if (error.code === "ENOENT")
|
|
281
|
+
return null;
|
|
282
|
+
throw error;
|
|
283
|
+
} };
|
|
284
|
+
const logBefore = log();
|
|
285
|
+
if (logBefore && (!old(logBefore) || logBefore.size !== 0))
|
|
286
|
+
return null;
|
|
287
|
+
const same = (a, b) => b.isFile()
|
|
288
|
+
&& a.dev === b.dev && a.ino === b.ino && a.uid === b.uid && a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
|
|
289
|
+
return () => {
|
|
290
|
+
try {
|
|
291
|
+
const logAfter = log();
|
|
292
|
+
return same(before, lstatSync(path)) && (logBefore ? !!logAfter && same(logBefore, logAfter) : logAfter === null);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/** An abandoned pin is recoverable only while every later unowned same-cwd alternative remains empty. Leave all of them untouched. */
|
|
304
|
+
function proveNoReplacementWork(input, plannedArchive) {
|
|
305
|
+
const original = JSON.parse(snapshotKiroTuiFile(join(input.sessionsDir, `${input.sessionId}.json`), 8 * 1024 * 1024).text);
|
|
306
|
+
const created = Date.parse(original.created_at);
|
|
307
|
+
if (!Number.isFinite(created))
|
|
308
|
+
throw new Error("Carried creation time is unavailable");
|
|
309
|
+
const unknownChecks = [];
|
|
310
|
+
for (const name of readdirSync(input.sessionsDir)) {
|
|
311
|
+
if (!name.endsWith(".json") || name === `${input.sessionId}.json`)
|
|
312
|
+
continue;
|
|
313
|
+
let file, meta;
|
|
314
|
+
try {
|
|
315
|
+
file = snapshotKiroTuiFile(join(input.sessionsDir, name), 8 * 1024 * 1024);
|
|
316
|
+
meta = JSON.parse(file.text);
|
|
317
|
+
if (!meta || typeof meta.cwd !== "string" || !meta.cwd.trim())
|
|
318
|
+
throw new Error("Unknown cwd");
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
const unchanged = oldUnknownMetadata(input, name, created);
|
|
322
|
+
if (!unchanged)
|
|
323
|
+
throw new Error(`Replacement session metadata ${JSON.stringify(name)} has unknown cwd and may contain newer work; automatic repair held`);
|
|
324
|
+
unknownChecks.push({ name, unchanged });
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (kiroCanonicalDirectory(meta.cwd) !== kiroCanonicalDirectory(input.workingDirectory))
|
|
328
|
+
continue;
|
|
329
|
+
if (Date.parse(meta.created_at) < created)
|
|
330
|
+
continue;
|
|
331
|
+
if (!safeId.test(meta.session_id) || name !== `${meta.session_id}.json`)
|
|
332
|
+
throw new Error("Replacement session metadata is ambiguous");
|
|
333
|
+
if (meta.session_id === plannedArchive)
|
|
334
|
+
continue; // read-only planning only; the confirmed operation needs the durable exact archive below.
|
|
335
|
+
const archived = archivedKiroAlternative(input, meta.session_id);
|
|
336
|
+
if (archived) {
|
|
337
|
+
unknownChecks.push({ name, unchanged: archived });
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const claim = join(input.agendHome, "kiro-identity", "claims", "v2", meta.session_id);
|
|
341
|
+
if (existsSync(claim)) {
|
|
342
|
+
const owner = readFileSync(claim, "utf8");
|
|
343
|
+
if (/^[^\n\r\0]{1,512}\n$/.test(owner) && owner !== input.instance + "\n")
|
|
344
|
+
continue;
|
|
345
|
+
throw new Error("A replacement session is already owned; automatic repair held");
|
|
346
|
+
}
|
|
347
|
+
const logName = `${meta.session_id}.jsonl`;
|
|
348
|
+
const work = (bytes) => new Error(`Newer same-cwd session ${JSON.stringify(logName)} has work (${bytes} bytes); choose which conversation to keep before repairing`);
|
|
349
|
+
let log;
|
|
350
|
+
try {
|
|
351
|
+
// This is an emptiness probe, not the ordinary 64 MiB transcript scan.
|
|
352
|
+
log = snapshotKiroTuiFile(join(input.sessionsDir, logName), 1);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (error instanceof KiroTuiSnapshotSizeError)
|
|
356
|
+
throw work(error.actualBytes);
|
|
357
|
+
throw new Error(`Replacement transcript ${JSON.stringify(logName)} could not be read safely; automatic repair held: ${error.message}`);
|
|
358
|
+
}
|
|
359
|
+
if (log.text.length !== 0)
|
|
360
|
+
throw work(Buffer.byteLength(log.text));
|
|
361
|
+
if (!file.unchanged() || !log.unchanged())
|
|
362
|
+
throw new Error(`Replacement transcript ${JSON.stringify(logName)} changed; automatic repair held`);
|
|
363
|
+
}
|
|
364
|
+
for (const check of unknownChecks)
|
|
365
|
+
if (!check.unchanged())
|
|
366
|
+
throw new Error(`Replacement session metadata ${JSON.stringify(check.name)} changed during proof; automatic repair held`);
|
|
367
|
+
}
|
|
368
|
+
/** This runs before discovery can adopt one of the empty sessions left by the old retry escape hatch. */
|
|
369
|
+
export function repairAbandonedKiroCarry(input, stopped = () => assertKiroConversationStopped(input)) {
|
|
370
|
+
const candidate = kiroCarryCandidate(input);
|
|
371
|
+
if (!candidate || candidate.id !== input.sessionId || !candidate.repair)
|
|
372
|
+
return false;
|
|
373
|
+
stopped();
|
|
374
|
+
proveOwnedCarry(input, true);
|
|
375
|
+
proveNoReplacementWork(input);
|
|
376
|
+
stopped();
|
|
377
|
+
reinstateKiroCarryPin(input, input.sessionId);
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
function validateInput(input) {
|
|
381
|
+
if (!safeId.test(input.sessionId) || !/^[^\n\r\0]{1,512}$/.test(input.instance))
|
|
382
|
+
throw new Error("Unsafe conversation id");
|
|
383
|
+
}
|
|
384
|
+
/** Archive all literal/canonical aliases of this exact conversation in this directory, never other conversations. */
|
|
385
|
+
export function archiveKiroClassicCopy(input, hooks = {}) {
|
|
386
|
+
validateInput(input);
|
|
387
|
+
if (!existsSync(input.dbPath))
|
|
388
|
+
return null;
|
|
389
|
+
ownedFile(input.dbPath);
|
|
390
|
+
const keys = kiroDirectoryKeys(input.workingDirectory), placeholders = keys.map(() => "?").join(",");
|
|
391
|
+
const db = new Database(input.dbPath, { fileMustExist: true, timeout: hooks.timeoutMs ?? 2000 });
|
|
392
|
+
const stopped = hooks.stopped ?? (() => assertKiroConversationStopped(input));
|
|
393
|
+
try {
|
|
394
|
+
const sizes = () => db.prepare(`SELECT length(CAST(value AS BLOB)) AS bytes FROM conversations_v2 WHERE key IN (${placeholders}) AND conversation_id=?`).all(...keys, input.sessionId);
|
|
395
|
+
if (!sizes().length)
|
|
396
|
+
return null;
|
|
397
|
+
stopped();
|
|
398
|
+
proveOwnedCarry(input);
|
|
399
|
+
hooks.checkpoint?.();
|
|
400
|
+
db.exec("BEGIN IMMEDIATE");
|
|
401
|
+
try {
|
|
402
|
+
const lengths = sizes();
|
|
403
|
+
if (!lengths.length) {
|
|
404
|
+
db.exec("COMMIT");
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
if (lengths.reduce((n, r) => n + r.bytes, 0) > MAX_ROW_BYTES)
|
|
408
|
+
throw new Error("Frozen classic rows exceed the archive limit");
|
|
409
|
+
const rows = db.prepare(`SELECT key, conversation_id AS id, CAST(value AS BLOB) AS value, created_at AS createdAt, updated_at AS updatedAt FROM conversations_v2 WHERE key IN (${placeholders}) AND conversation_id=? ORDER BY key`).all(...keys, input.sessionId);
|
|
410
|
+
if (hooks.classicHash !== undefined && fingerprintClassicRows(rows.map(r => ({ key: r.key, conversation_id: r.id, value: r.value.toString("hex").toUpperCase(), created_at: r.createdAt, updated_at: r.updatedAt }))) !== hooks.classicHash)
|
|
411
|
+
throw new Error("Confirmed classic bytes changed; reconciliation held");
|
|
412
|
+
const path = writeArchive(input, rows.map(r => ({ key: r.key, id: r.id, valueBase64: r.value.toString("base64"), createdAt: r.createdAt, updatedAt: r.updatedAt })));
|
|
413
|
+
hooks.checkpoint?.();
|
|
414
|
+
stopped();
|
|
415
|
+
proveOwnedCarry(input);
|
|
416
|
+
const del = db.prepare("DELETE FROM conversations_v2 WHERE key=? AND conversation_id=? AND CAST(value AS BLOB)=? AND created_at=? AND updated_at=?");
|
|
417
|
+
for (const row of rows)
|
|
418
|
+
if (del.run(row.key, row.id, row.value, row.createdAt, row.updatedAt).changes !== 1)
|
|
419
|
+
throw new Error("Frozen classic row changed; nothing removed");
|
|
420
|
+
hooks.checkpoint?.();
|
|
421
|
+
db.exec("COMMIT");
|
|
422
|
+
return path;
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
if (db.inTransaction)
|
|
426
|
+
db.exec("ROLLBACK");
|
|
427
|
+
throw err;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
db.close();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/** One automatic launch repair. Explicit fresh starts do not call this. */
|
|
435
|
+
export function prepareKiroCarryForLaunch(input, checkpoint) {
|
|
436
|
+
const pendingReconciliation = assertKiroReconciliationLaunch({ ...input, sessionId: "unresolved" });
|
|
437
|
+
if (pendingReconciliation) {
|
|
438
|
+
assertPreparedClassicSource(pendingReconciliation.input, pendingReconciliation.classicHash);
|
|
439
|
+
// Re-pinning may already have survived an interrupted attempt. Its current
|
|
440
|
+
// candidate no longer says repair, but unacknowledged later work still holds.
|
|
441
|
+
proveNoReplacementWork(pendingReconciliation.input);
|
|
442
|
+
}
|
|
443
|
+
const candidate = kiroCarryCandidate(input);
|
|
444
|
+
if (!candidate)
|
|
445
|
+
return { sessionId: null, archivePath: null, repaired: false };
|
|
446
|
+
const full = { ...input, sessionId: candidate.id };
|
|
447
|
+
let repaired = false;
|
|
448
|
+
if (candidate.repair) {
|
|
449
|
+
if (!existsSync(input.dbPath))
|
|
450
|
+
throw new Error("No frozen classic copy proves this abandoned migration; repair held");
|
|
451
|
+
ownedFile(input.dbPath);
|
|
452
|
+
const db = new Database(input.dbPath, { readonly: true, fileMustExist: true });
|
|
453
|
+
let present;
|
|
454
|
+
try {
|
|
455
|
+
const keys = kiroDirectoryKeys(input.workingDirectory);
|
|
456
|
+
present = !!db.prepare(`SELECT 1 FROM conversations_v2 WHERE key IN (${keys.map(() => "?").join(",")}) AND conversation_id=? LIMIT 1`).get(...keys, candidate.id);
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
db.close();
|
|
460
|
+
}
|
|
461
|
+
if (!present) {
|
|
462
|
+
const root = join(input.agendHome, "kiro-classic-archives", hash(input.instance).slice(0, 32));
|
|
463
|
+
const completed = existsSync(root) && readdirSync(root).some(name => {
|
|
464
|
+
if (!name.startsWith(candidate.id + "-") || !name.endsWith(".json"))
|
|
465
|
+
return false;
|
|
466
|
+
const archive = readArchive(join(root, name)).payload;
|
|
467
|
+
return archive.instance === input.instance && archive.sessionId === candidate.id && archive.dbPath === resolve(input.dbPath);
|
|
468
|
+
});
|
|
469
|
+
if (!completed)
|
|
470
|
+
throw new Error("No frozen classic row or verified archive proves this abandoned migration; repair held");
|
|
471
|
+
if (!pendingReconciliation)
|
|
472
|
+
return { sessionId: null, archivePath: null, repaired: false }; // deliberate fresh after an already archived migration
|
|
473
|
+
}
|
|
474
|
+
checkpoint?.();
|
|
475
|
+
repaired = repairAbandonedKiroCarry(full);
|
|
476
|
+
}
|
|
477
|
+
checkpoint?.();
|
|
478
|
+
proveOwnedCarry(full);
|
|
479
|
+
const archivePath = archiveKiroClassicCopy(full, { checkpoint, classicHash: pendingReconciliation?.classicHash });
|
|
480
|
+
if (pendingReconciliation)
|
|
481
|
+
assertPreparedClassicSource(pendingReconciliation.input, pendingReconciliation.classicHash);
|
|
482
|
+
checkpoint?.();
|
|
483
|
+
finishKiroReconciliationLaunch(full);
|
|
484
|
+
return { sessionId: candidate.id, archivePath, repaired };
|
|
485
|
+
}
|
|
486
|
+
function reconcileStopped(input) {
|
|
487
|
+
assertKiroConversationStopped(input);
|
|
488
|
+
assertKiroConversationStopped({ ...input, sessionId: input.archiveId });
|
|
489
|
+
}
|
|
490
|
+
function fingerprintClassicRows(rows) {
|
|
491
|
+
return hash(JSON.stringify(rows.slice().sort((a, b) => Buffer.compare(Buffer.from(a.key), Buffer.from(b.key)))));
|
|
492
|
+
}
|
|
493
|
+
function classicRows(input) {
|
|
494
|
+
const db = new Database(input.dbPath, { readonly: true, fileMustExist: true, timeout: 200 });
|
|
495
|
+
try {
|
|
496
|
+
const keys = kiroDirectoryKeys(input.workingDirectory);
|
|
497
|
+
const where = `key IN (${keys.map(() => "?").join(",")}) AND conversation_id=?`;
|
|
498
|
+
const sizes = db.prepare(`SELECT length(CAST(value AS BLOB)) AS n FROM conversations_v2 WHERE ${where}`).all(...keys, input.sessionId);
|
|
499
|
+
if (sizes.reduce((n, r) => n + r.n, 0) > MAX_ROW_BYTES)
|
|
500
|
+
throw new Error("Confirmed classic rows exceed the archive limit");
|
|
501
|
+
return db.prepare(`SELECT key, conversation_id, hex(CAST(value AS BLOB)) AS value, created_at, updated_at FROM conversations_v2 WHERE ${where} ORDER BY key`).all(...keys, input.sessionId);
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
db.close();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function classicHash(input) { return fingerprintClassicRows(classicRows(input)); }
|
|
508
|
+
function matchingClassicArchive(input, expected) {
|
|
509
|
+
const root = join(input.agendHome, "kiro-classic-archives", hash(input.instance).slice(0, 32));
|
|
510
|
+
if (!existsSync(root))
|
|
511
|
+
return undefined;
|
|
512
|
+
return readdirSync(root).filter(name => name.startsWith(input.sessionId + "-") && name.endsWith(".json"))
|
|
513
|
+
.map(name => join(root, name)).find(path => {
|
|
514
|
+
const m = kiroClassicArchiveMetadata(path);
|
|
515
|
+
return m.instance === input.instance && m.sessionId === input.sessionId && m.directory === kiroCanonicalDirectory(input.workingDirectory)
|
|
516
|
+
&& m.profile === input.credentialProfile && m.dbPath === resolve(input.dbPath)
|
|
517
|
+
&& fingerprintClassicRows(readArchive(path).payload.rows.map(r => ({ key: r.key, conversation_id: r.id,
|
|
518
|
+
value: Buffer.from(r.valueBase64, "base64").toString("hex").toUpperCase(), created_at: r.createdAt, updated_at: r.updatedAt }))) === expected;
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
function assertPreparedClassicSource(input, expected) {
|
|
522
|
+
const rows = classicRows(input);
|
|
523
|
+
if (fingerprintClassicRows(rows) === expected)
|
|
524
|
+
return;
|
|
525
|
+
if (!rows.length && matchingClassicArchive(input, expected))
|
|
526
|
+
return; // removal committed, final selection marker did not
|
|
527
|
+
throw new Error("Confirmed classic bytes changed or their exact archive is missing; reconciliation held");
|
|
528
|
+
}
|
|
529
|
+
/** Runs on the bounded store worker. Never writes while planning or before the admin's confirmation. */
|
|
530
|
+
export function planKiroCarryReconciliation(input, rollbackReceipt) {
|
|
531
|
+
reconcileStopped(input);
|
|
532
|
+
if (rollbackReceipt)
|
|
533
|
+
return inspectKiroReconciliationRollback(input, rollbackReceipt);
|
|
534
|
+
proveOwnedCarry(input, true);
|
|
535
|
+
const plan = inspectKiroReconciliation(input, classicHash(input));
|
|
536
|
+
proveNoReplacementWork(input, input.archiveId);
|
|
537
|
+
reconcileStopped(input);
|
|
538
|
+
return plan;
|
|
539
|
+
}
|
|
540
|
+
export function applyKiroCarryReconciliation(input, plan, actor, checkpoint) {
|
|
541
|
+
checkpoint();
|
|
542
|
+
reconcileStopped(input);
|
|
543
|
+
const current = planKiroCarryReconciliation(input, plan.rollbackReceipt);
|
|
544
|
+
if (current.digest !== plan.digest)
|
|
545
|
+
throw new Error("Reconciliation confirmation is stale");
|
|
546
|
+
if (plan.rollbackReceipt) {
|
|
547
|
+
if (!actor || actor.length > 512)
|
|
548
|
+
throw new Error("Rollback actor is required");
|
|
549
|
+
checkpoint();
|
|
550
|
+
reconcileStopped(input);
|
|
551
|
+
const rollback = beginKiroReconciliationRollback(input, plan);
|
|
552
|
+
if (classicHash(input) !== rollback.classicHash) {
|
|
553
|
+
const archive = matchingClassicArchive(input, rollback.classicHash);
|
|
554
|
+
if (!archive)
|
|
555
|
+
throw new Error("The original classic archive is required for rollback");
|
|
556
|
+
checkpoint();
|
|
557
|
+
restoreKiroClassicArchive(input, archive);
|
|
558
|
+
if (classicHash(input) !== rollback.classicHash)
|
|
559
|
+
throw new Error("Original classic bytes changed; rollback remains held");
|
|
560
|
+
}
|
|
561
|
+
checkpoint();
|
|
562
|
+
reconcileStopped(input);
|
|
563
|
+
return rollbackKiroSelection(input, rollback.plan);
|
|
564
|
+
}
|
|
565
|
+
const receipt = preserveKiroReconciliation(input, plan, classicHash(input), actor);
|
|
566
|
+
checkpoint();
|
|
567
|
+
reconcileStopped(input);
|
|
568
|
+
const prepared = prepareKiroCarryForLaunch(input, checkpoint);
|
|
569
|
+
if (prepared.sessionId !== input.sessionId)
|
|
570
|
+
throw new Error("Reconciliation did not retain the original conversation");
|
|
571
|
+
checkpoint();
|
|
572
|
+
reconcileStopped(input);
|
|
573
|
+
completeKiroSelection(input, receipt);
|
|
574
|
+
return receipt;
|
|
575
|
+
}
|
|
576
|
+
/** Operator recovery only: verify the archive and insert missing exact rows. Never overwrite newer/different data. */
|
|
577
|
+
export function restoreKiroClassicArchive(input, path, stopped = () => assertKiroConversationStopped(input)) {
|
|
578
|
+
validateInput(input);
|
|
579
|
+
const { payload } = readArchive(path);
|
|
580
|
+
if (payload.instance !== input.instance || payload.sessionId !== input.sessionId || payload.directory !== kiroCanonicalDirectory(input.workingDirectory)
|
|
581
|
+
|| payload.profile !== input.credentialProfile || payload.dbPath !== resolve(input.dbPath) || !Array.isArray(payload.rows) || !payload.rows.length)
|
|
582
|
+
throw new Error("Archive does not match this instance/store");
|
|
583
|
+
const keys = new Set(kiroDirectoryKeys(input.workingDirectory));
|
|
584
|
+
for (const row of payload.rows)
|
|
585
|
+
if (!keys.has(row.key) || row.id !== input.sessionId || typeof row.valueBase64 !== "string"
|
|
586
|
+
|| Buffer.from(row.valueBase64, "base64").toString("base64") !== row.valueBase64 || !Number.isSafeInteger(row.createdAt) || !Number.isSafeInteger(row.updatedAt))
|
|
587
|
+
throw new Error("Archive row is invalid");
|
|
588
|
+
stopped();
|
|
589
|
+
ownedFile(input.dbPath);
|
|
590
|
+
const db = new Database(input.dbPath, { fileMustExist: true, timeout: 2000 });
|
|
591
|
+
try {
|
|
592
|
+
db.exec("BEGIN IMMEDIATE");
|
|
593
|
+
try {
|
|
594
|
+
stopped();
|
|
595
|
+
for (const row of payload.rows) {
|
|
596
|
+
const old = db.prepare("SELECT CAST(value AS BLOB) AS value, created_at AS createdAt, updated_at AS updatedAt FROM conversations_v2 WHERE key=? AND conversation_id=?").get(row.key, row.id);
|
|
597
|
+
const bytes = Buffer.from(row.valueBase64, "base64");
|
|
598
|
+
if (old) {
|
|
599
|
+
if (!old.value.equals(bytes) || old.createdAt !== row.createdAt || old.updatedAt !== row.updatedAt)
|
|
600
|
+
throw new Error("A different classic row already exists; restore refused");
|
|
601
|
+
}
|
|
602
|
+
else
|
|
603
|
+
db.prepare("INSERT INTO conversations_v2 (key,conversation_id,value,created_at,updated_at) VALUES (?,?,CAST(? AS TEXT),?,?)").run(row.key, row.id, bytes, row.createdAt, row.updatedAt);
|
|
604
|
+
}
|
|
605
|
+
db.exec("COMMIT");
|
|
606
|
+
}
|
|
607
|
+
catch (err) {
|
|
608
|
+
if (db.inTransaction)
|
|
609
|
+
db.exec("ROLLBACK");
|
|
610
|
+
throw err;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
finally {
|
|
614
|
+
db.close();
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/** Metadata only, for the operator CLI to resolve the current configured store before restoration. */
|
|
618
|
+
export function kiroClassicArchiveMetadata(path) {
|
|
619
|
+
const p = readArchive(path).payload;
|
|
620
|
+
return { instance: p.instance, sessionId: p.sessionId, directory: p.directory, profile: p.profile, dbPath: p.dbPath };
|
|
621
|
+
}
|
|
622
|
+
//# sourceMappingURL=kiro-classic-archive.js.map
|