mirror-mind 0.36.2 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/extensions/mirror-logger.ts +293 -64
- package/.pi/skills/mm-help/SKILL.md +4 -1
- package/.pi/skills/mm-mirror/SKILL.md +25 -6
- package/README.md +3 -3
- package/docs/releases/index.md +2 -0
- package/docs/releases/v0.36.3.md +142 -0
- package/docs/releases/v0.37.0.md +221 -0
- package/package.json +1 -1
- package/plugins/mirror-mind/.claude-plugin/plugin.json +1 -1
- package/plugins/mirror-mind/plugin.json +1 -1
- package/plugins/mirror-mind/skills/mm-mirror/SKILL.md +14 -6
- package/ts/src/builder/commands.ts +9 -6
- package/ts/src/builder/load.ts +14 -2
- package/ts/src/conversation/commandTurn.ts +3 -2
- package/ts/src/conversation/logger.ts +12 -6
- package/ts/src/conversation/piSkillTurns.ts +267 -0
- package/ts/src/db/migrateOnOpen.ts +1 -1
- package/ts/src/db/migrations.ts +16 -0
- package/ts/src/db/schema.ts +0 -1
- package/ts/src/db/schemaInventorySnapshot.ts +1 -8
- package/ts/src/db/schemaState.ts +2 -0
- package/ts/src/db/schemaTsDivergence.ts +25 -4
- package/ts/src/frontDoor/cli.ts +58 -2
- package/ts/src/frontDoor/conversationLoggerRoute.ts +20 -2
- package/ts/src/frontDoor/exploreRoute.ts +2 -2
- package/ts/src/frontDoor/mirrorModeRoute.ts +129 -5
- package/ts/src/frontDoor/routing.ts +26 -15
- package/ts/src/frontDoor/skillTurnsRoute.ts +213 -0
- package/ts/src/frontDoor/soulRoute.ts +4 -4
- package/ts/src/frontDoor/stdinSource.ts +5 -0
- package/ts/src/guards/testTempHomes.ts +92 -0
- package/ts/src/hooks/agy.ts +136 -59
- package/ts/src/hooks/claude.ts +29 -90
- package/ts/src/hooks/mirrorState.ts +10 -22
- package/ts/src/mirror/context.ts +19 -4
- package/ts/src/mirror/defaultResolution.ts +2 -1
- package/ts/src/mirror/doorman.ts +420 -0
- package/ts/src/mirror/mirrorForm.ts +4 -1
- package/ts/src/mirror/orchestration.ts +22 -3
- package/ts/src/mirror/render.ts +7 -1
- package/ts/src/mirror/runtimeSession.ts +66 -8
- package/ts/src/mode/operatingMode.ts +22 -4
- package/ts/src/providers/transport.ts +14 -0
- package/ts/src/runtime/diagnose.ts +32 -7
- package/ts/src/runtime/pluginWiring.ts +6 -4
- package/ts/src/runtime/status.ts +3 -2
- package/ts/src/runtime/update.ts +17 -1
- package/ts/src/runtime/wiring.ts +14 -1
- package/ts/src/search/memorySearch.ts +38 -18
- package/ts/src/search/ranker.ts +16 -2
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// The Pi user turns an earlier version stored as a skill's body, and their repair
|
|
2
|
+
// (CR160).
|
|
3
|
+
//
|
|
4
|
+
// Pi turns a prompt that opens with `/skill:<name>`, which is also what its command
|
|
5
|
+
// menu sends, into the skill's whole body before its extensions see it. Until CR160
|
|
6
|
+
// Pi's extension logged that prompt, so a skill invocation was stored as
|
|
7
|
+
// `<skill name="…" location="…">` and the body, as if the person had said it; every
|
|
8
|
+
// later reader -- the title, the embedding, extraction -- read it so.
|
|
9
|
+
//
|
|
10
|
+
// The repair applies the rule the core logs by (`commandTurn.ts`): a Mirror
|
|
11
|
+
// question keeps its question, without the block (`mirrorForm.ts`); any other skill
|
|
12
|
+
// invocation is not conversation, and neither are its arguments (a `mm-build`
|
|
13
|
+
// slug), so its row goes. A conversation the repair leaves with no user turn goes
|
|
14
|
+
// too, through the shared deletion, which keeps its memories unlinked (the
|
|
15
|
+
// Navigator's decision, A7). A conversation titled from a block, and not titled by
|
|
16
|
+
// hand, gets the title every conversation gets from its first user turn, marked
|
|
17
|
+
// provisional so the title paths can improve it. Replies stay: they are what the
|
|
18
|
+
// mirror said, and a reply to a skipped turn cannot be told from another. Summaries
|
|
19
|
+
// and their embeddings stay: they describe a session in which a skill was invoked
|
|
20
|
+
// (A6).
|
|
21
|
+
//
|
|
22
|
+
// Only Pi's whole wrapper, only in Pi conversations (A8): a person may type or
|
|
23
|
+
// paste `<skill name=` at the start of a message anywhere. One more shape counts:
|
|
24
|
+
// the wrapper the old logger cut at its size limit, whose ending only it writes.
|
|
25
|
+
|
|
26
|
+
import { isCommandTurn } from "#conversation/commandTurn.ts";
|
|
27
|
+
import { deleteConversation } from "#conversation/conversationDeletion.ts";
|
|
28
|
+
import { generateTitle, setProvisionalTitle } from "#conversation/logger.ts";
|
|
29
|
+
import { type Database, type WritableDatabase, withTransaction } from "#db/database.ts";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pi's expansion of `/skill:<name> args`, whole: the opening tag, Pi's line naming
|
|
33
|
+
* where the skill's references resolve, the body, the closing tag, and the
|
|
34
|
+
* arguments after a blank line when there are any (Pi 1.3.0 and 1.4.0,
|
|
35
|
+
* `_expandSkillCommand`). Pi's own extension reads the same shape back
|
|
36
|
+
* (`.pi/extensions/mirror-logger.ts`); `piSkillTurns.test.ts` holds the two alike.
|
|
37
|
+
*/
|
|
38
|
+
export const PI_SKILL_WRAPPER =
|
|
39
|
+
/^<skill name="([^"]+)" location="[^"]*">\nReferences are relative to [^\n]*\n\n[\s\S]*?\n<\/skill>(?:\n\n([\s\S]*))?$/;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The same block when the old logger cut it: the extension kept the first 50,000
|
|
43
|
+
* characters and added its own marker (`truncate`, `.pi/extensions/mirror-logger.ts`),
|
|
44
|
+
* so the closing tag never arrived. Pi's opening lines, then an ending only the
|
|
45
|
+
* logger writes: no person types that. Whatever followed the block was cut with it.
|
|
46
|
+
*/
|
|
47
|
+
const PI_SKILL_WRAPPER_CUT =
|
|
48
|
+
/^<skill name="([^"]+)" location="[^"]*">\nReferences are relative to [^\n]*\n\n[\s\S]*\n\[… truncated\]$/;
|
|
49
|
+
|
|
50
|
+
/** The one skill whose turn is conversation: its question is the turn. */
|
|
51
|
+
const MIRROR_SKILL = "mm-mirror";
|
|
52
|
+
|
|
53
|
+
export interface StoredSkillTurn {
|
|
54
|
+
messageId: string;
|
|
55
|
+
conversationId: string;
|
|
56
|
+
skill: string;
|
|
57
|
+
/** What followed the block, trimmed; "" when nothing did. */
|
|
58
|
+
tail: string;
|
|
59
|
+
/** The Mirror question the row keeps, or null when the row goes. */
|
|
60
|
+
question: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ConversationToDelete {
|
|
64
|
+
id: string;
|
|
65
|
+
title: string | null;
|
|
66
|
+
messageCount: number;
|
|
67
|
+
startedAt: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ConversationToRetitle {
|
|
71
|
+
id: string;
|
|
72
|
+
title: string;
|
|
73
|
+
newTitle: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SkillTurnRepairPlan {
|
|
77
|
+
/** Every stored skill turn, in the order the rows were written. */
|
|
78
|
+
turns: StoredSkillTurn[];
|
|
79
|
+
/** The Mirror questions: each row keeps its question. */
|
|
80
|
+
kept: StoredSkillTurn[];
|
|
81
|
+
/** Every other skill turn: each row goes. */
|
|
82
|
+
removed: StoredSkillTurn[];
|
|
83
|
+
conversationsToDelete: ConversationToDelete[];
|
|
84
|
+
conversationsToRetitle: ConversationToRetitle[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface SkillTurnRepairResult {
|
|
88
|
+
kept: number;
|
|
89
|
+
removed: number;
|
|
90
|
+
conversationsDeleted: number;
|
|
91
|
+
/** Messages that went with the deleted conversations, beyond the removed turns. */
|
|
92
|
+
messagesDeletedWithConversations: number;
|
|
93
|
+
conversationsRetitled: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The set the transaction found is not the set the person was shown. */
|
|
97
|
+
export class SkillTurnRepairSetMovedError extends Error {
|
|
98
|
+
constructor() {
|
|
99
|
+
super("the stored skill turns changed since they were listed");
|
|
100
|
+
this.name = "SkillTurnRepairSetMovedError";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The skill turn a stored row holds, or null when the row is not Pi's whole wrapper. */
|
|
105
|
+
export function readSkillTurn(content: string): { skill: string; tail: string } | null {
|
|
106
|
+
const match = PI_SKILL_WRAPPER.exec(content);
|
|
107
|
+
if (match) return { skill: match[1] as string, tail: (match[2] ?? "").trim() };
|
|
108
|
+
const cut = PI_SKILL_WRAPPER_CUT.exec(content);
|
|
109
|
+
if (cut) return { skill: cut[1] as string, tail: "" };
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isManualTitle(rawMetadata: unknown): boolean {
|
|
114
|
+
if (typeof rawMetadata !== "string") return false;
|
|
115
|
+
try {
|
|
116
|
+
const metadata = JSON.parse(rawMetadata) as Record<string, unknown>;
|
|
117
|
+
return metadata.title_status === "manual" || metadata.title_source === "manual";
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* What the repair would change, decided once for the dry run and again inside the
|
|
125
|
+
* repair's transaction.
|
|
126
|
+
*/
|
|
127
|
+
export function planSkillTurnRepair(db: Database): SkillTurnRepairPlan {
|
|
128
|
+
// SQL narrows to the prefix; `readSkillTurn` decides on the whole wrapper.
|
|
129
|
+
const candidates = db
|
|
130
|
+
.prepare(
|
|
131
|
+
`SELECT m.id, m.conversation_id, m.content
|
|
132
|
+
FROM messages m JOIN conversations c ON c.id = m.conversation_id
|
|
133
|
+
WHERE c.interface = 'pi' AND m.role = 'user' AND substr(m.content, 1, 13) = '<skill name="'
|
|
134
|
+
ORDER BY m.created_at, m.rowid`,
|
|
135
|
+
)
|
|
136
|
+
.all();
|
|
137
|
+
const turns: StoredSkillTurn[] = [];
|
|
138
|
+
for (const row of candidates) {
|
|
139
|
+
const read = readSkillTurn(String(row.content));
|
|
140
|
+
if (read === null) continue;
|
|
141
|
+
const question = read.skill === MIRROR_SKILL && read.tail !== "" ? read.tail : null;
|
|
142
|
+
turns.push({
|
|
143
|
+
messageId: String(row.id),
|
|
144
|
+
conversationId: String(row.conversation_id),
|
|
145
|
+
skill: read.skill,
|
|
146
|
+
tail: read.tail,
|
|
147
|
+
question,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const kept = turns.filter((turn) => turn.question !== null);
|
|
151
|
+
const removed = turns.filter((turn) => turn.question === null);
|
|
152
|
+
const removedIds = new Set(removed.map((turn) => turn.messageId));
|
|
153
|
+
const keptQuestion = new Map(kept.map((turn) => [turn.messageId, turn.question as string]));
|
|
154
|
+
|
|
155
|
+
// A conversation the repair leaves with no user turn goes. Otherwise a block-born
|
|
156
|
+
// title is rebuilt from its first remaining user turn that is not a command: a
|
|
157
|
+
// command is not conversation (`commandTurn.ts`), so it names one only when
|
|
158
|
+
// nothing else remains.
|
|
159
|
+
const touched = [...new Set(turns.map((turn) => turn.conversationId))];
|
|
160
|
+
const userRows = db.prepare(
|
|
161
|
+
`SELECT id, content FROM messages WHERE conversation_id = ? AND role = 'user'
|
|
162
|
+
ORDER BY created_at, rowid`,
|
|
163
|
+
);
|
|
164
|
+
const remainingUserTurns = (conversationId: string, removing: ReadonlySet<string>): string[] =>
|
|
165
|
+
userRows
|
|
166
|
+
.all(conversationId)
|
|
167
|
+
.filter((row) => !removing.has(String(row.id)))
|
|
168
|
+
.map((row) => keptQuestion.get(String(row.id)) ?? String(row.content))
|
|
169
|
+
.filter((content) => content.trim() !== "");
|
|
170
|
+
const titleSource = (remaining: readonly string[]): string | null =>
|
|
171
|
+
remaining.find((content) => !isCommandTurn(content)) ?? remaining[0] ?? null;
|
|
172
|
+
const remainingFirstUser = new Map<string, string | null>();
|
|
173
|
+
for (const conversationId of touched) {
|
|
174
|
+
remainingFirstUser.set(
|
|
175
|
+
conversationId,
|
|
176
|
+
titleSource(remainingUserTurns(conversationId, removedIds)),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const conversationRow = db.prepare(
|
|
181
|
+
`SELECT id, title, started_at, metadata,
|
|
182
|
+
(SELECT COUNT(*) FROM messages m WHERE m.conversation_id = c.id) AS message_count
|
|
183
|
+
FROM conversations c WHERE id = ?`,
|
|
184
|
+
);
|
|
185
|
+
const conversationsToDelete: ConversationToDelete[] = [];
|
|
186
|
+
for (const conversationId of touched) {
|
|
187
|
+
if (remainingFirstUser.get(conversationId) !== null) continue;
|
|
188
|
+
const row = conversationRow.get(conversationId);
|
|
189
|
+
if (!row) continue;
|
|
190
|
+
conversationsToDelete.push({
|
|
191
|
+
id: conversationId,
|
|
192
|
+
title: typeof row.title === "string" ? row.title : null,
|
|
193
|
+
messageCount: Number(row.message_count),
|
|
194
|
+
startedAt: String(row.started_at),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const deleting = new Set(conversationsToDelete.map((item) => item.id));
|
|
198
|
+
|
|
199
|
+
// Titles born of a block, in any Pi conversation that stays, unless set by hand.
|
|
200
|
+
const titled = db
|
|
201
|
+
.prepare(
|
|
202
|
+
`SELECT id, title, metadata FROM conversations
|
|
203
|
+
WHERE interface = 'pi' AND lower(substr(trim(title), 1, 6)) = '<skill'
|
|
204
|
+
ORDER BY started_at, id`,
|
|
205
|
+
)
|
|
206
|
+
.all();
|
|
207
|
+
const conversationsToRetitle: ConversationToRetitle[] = [];
|
|
208
|
+
for (const row of titled) {
|
|
209
|
+
const id = String(row.id);
|
|
210
|
+
if (deleting.has(id) || isManualTitle(row.metadata)) continue;
|
|
211
|
+
const source = remainingFirstUser.has(id)
|
|
212
|
+
? remainingFirstUser.get(id)
|
|
213
|
+
: titleSource(remainingUserTurns(id, new Set()));
|
|
214
|
+
if (!source || readSkillTurn(source) !== null) continue;
|
|
215
|
+
const newTitle = generateTitle(source).trim();
|
|
216
|
+
if (newTitle === "") continue;
|
|
217
|
+
conversationsToRetitle.push({ id, title: String(row.title), newTitle });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { turns, kept, removed, conversationsToDelete, conversationsToRetitle };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** One string that names everything a plan changes, to compare a plan with the one shown. */
|
|
224
|
+
export function skillTurnRepairKey(plan: SkillTurnRepairPlan): string {
|
|
225
|
+
const sorted = (values: readonly string[]) => [...values].sort().join(",");
|
|
226
|
+
return [
|
|
227
|
+
sorted(plan.kept.map((turn) => turn.messageId)),
|
|
228
|
+
sorted(plan.removed.map((turn) => turn.messageId)),
|
|
229
|
+
sorted(plan.conversationsToDelete.map((item) => item.id)),
|
|
230
|
+
sorted(plan.conversationsToRetitle.map((item) => `${item.id}=${item.newTitle}`)),
|
|
231
|
+
].join("|");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Repair what the plan names, in one transaction: all or nothing. The plan is
|
|
236
|
+
* decided again inside the transaction and must be the one the person was shown
|
|
237
|
+
* (`expectedKey`); if it moved, nothing changes.
|
|
238
|
+
*/
|
|
239
|
+
export function applySkillTurnRepair(
|
|
240
|
+
db: WritableDatabase,
|
|
241
|
+
expectedKey: string,
|
|
242
|
+
): SkillTurnRepairResult {
|
|
243
|
+
return withTransaction(db, () => {
|
|
244
|
+
const plan = planSkillTurnRepair(db);
|
|
245
|
+
if (skillTurnRepairKey(plan) !== expectedKey) throw new SkillTurnRepairSetMovedError();
|
|
246
|
+
const keepQuestion = db.prepare("UPDATE messages SET content = ? WHERE id = ?");
|
|
247
|
+
for (const turn of plan.kept) keepQuestion.run(turn.question as string, turn.messageId);
|
|
248
|
+
const removeTurn = db.prepare("DELETE FROM messages WHERE id = ?");
|
|
249
|
+
for (const turn of plan.removed) removeTurn.run(turn.messageId);
|
|
250
|
+
const remainingMessages = db.prepare(
|
|
251
|
+
"SELECT COUNT(*) AS n FROM messages WHERE conversation_id = ?",
|
|
252
|
+
);
|
|
253
|
+
let messagesDeletedWithConversations = 0;
|
|
254
|
+
for (const item of plan.conversationsToDelete) {
|
|
255
|
+
messagesDeletedWithConversations += Number(remainingMessages.get(item.id)?.n ?? 0);
|
|
256
|
+
deleteConversation(db, item.id);
|
|
257
|
+
}
|
|
258
|
+
for (const item of plan.conversationsToRetitle) setProvisionalTitle(db, item.id, item.newTitle);
|
|
259
|
+
return {
|
|
260
|
+
kept: plan.kept.length,
|
|
261
|
+
removed: plan.removed.length,
|
|
262
|
+
conversationsDeleted: plan.conversationsToDelete.length,
|
|
263
|
+
messagesDeletedWithConversations,
|
|
264
|
+
conversationsRetitled: plan.conversationsToRetitle.length,
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
}
|
|
@@ -132,7 +132,7 @@ function appliedMigrationIds(dbPath: string, options: OpenOptions): Set<string>
|
|
|
132
132
|
* No split any more. Until CV22.DS10.TS5 this separated the TS-authored tail
|
|
133
133
|
* from Python-authored ids and declined the whole thing whenever a Python one
|
|
134
134
|
* was pending -- correct while Python could still apply them. TypeScript
|
|
135
|
-
* carries all
|
|
135
|
+
* carries all of them and is the only engine left, so every pending known
|
|
136
136
|
* migration is simply pending.
|
|
137
137
|
*/
|
|
138
138
|
function pendingMigrations(applied: Set<string>): string[] {
|
package/ts/src/db/migrations.ts
CHANGED
|
@@ -514,6 +514,21 @@ function migrateJourneyParentColumn(db: WritableDatabase): void {
|
|
|
514
514
|
);
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
+
/**
|
|
518
|
+
* 018 (CV9.DS8.US5) -- `runtime_sessions.hook_injected` goes. It was the
|
|
519
|
+
* Claude-family inject hook's once-per-session flag (CV21.DS2); US4 handed the
|
|
520
|
+
* hook to the one doorman and retired its last reader, and the four writers
|
|
521
|
+
* that still filled it were carried as one debt item, paid here with the
|
|
522
|
+
* column. Guarded on the column's presence: a fresh database's `createSchema`
|
|
523
|
+
* no longer has it, and a re-run finds nothing to drop. `DROP COLUMN` is
|
|
524
|
+
* SQLite 3.35; the engine ships 3.54.
|
|
525
|
+
*/
|
|
526
|
+
function migrateDropHookInjected(db: WritableDatabase): void {
|
|
527
|
+
if (!tableExists(db, "runtime_sessions")) return;
|
|
528
|
+
if (!tableColumns(db, "runtime_sessions").has("hook_injected")) return;
|
|
529
|
+
db.exec("ALTER TABLE runtime_sessions DROP COLUMN hook_injected");
|
|
530
|
+
}
|
|
531
|
+
|
|
517
532
|
export type MigrationApply = (db: WritableDatabase) => void;
|
|
518
533
|
|
|
519
534
|
export const MIGRATIONS: readonly (readonly [string, MigrationApply])[] = [
|
|
@@ -534,6 +549,7 @@ export const MIGRATIONS: readonly (readonly [string, MigrationApply])[] = [
|
|
|
534
549
|
["015_create_builder_workbench", migrateCreateBuilderWorkbench],
|
|
535
550
|
["016_builder_workbench_display_codes", migrateBuilderWorkbenchDisplayCodes],
|
|
536
551
|
["017_journey_parent_column", migrateJourneyParentColumn],
|
|
552
|
+
["018_drop_hook_injected", migrateDropHookInjected],
|
|
537
553
|
];
|
|
538
554
|
|
|
539
555
|
/**
|
package/ts/src/db/schema.ts
CHANGED
|
@@ -84,7 +84,6 @@ CREATE TABLE IF NOT EXISTS runtime_sessions (
|
|
|
84
84
|
mirror_active INTEGER NOT NULL DEFAULT 0,
|
|
85
85
|
persona TEXT,
|
|
86
86
|
journey TEXT,
|
|
87
|
-
hook_injected INTEGER NOT NULL DEFAULT 0,
|
|
88
87
|
active INTEGER NOT NULL DEFAULT 1,
|
|
89
88
|
started_at TEXT NOT NULL,
|
|
90
89
|
updated_at TEXT NOT NULL,
|
|
@@ -1925,13 +1925,6 @@ export const SCHEMA_INVENTORY_SNAPSHOT: SchemaInventory = {
|
|
|
1925
1925
|
"pk": 0,
|
|
1926
1926
|
"type": "TEXT"
|
|
1927
1927
|
},
|
|
1928
|
-
{
|
|
1929
|
-
"dflt_value": "0",
|
|
1930
|
-
"name": "hook_injected",
|
|
1931
|
-
"notnull": 1,
|
|
1932
|
-
"pk": 0,
|
|
1933
|
-
"type": "INTEGER"
|
|
1934
|
-
},
|
|
1935
1928
|
{
|
|
1936
1929
|
"dflt_value": "1",
|
|
1937
1930
|
"name": "active",
|
|
@@ -1979,7 +1972,7 @@ export const SCHEMA_INVENTORY_SNAPSHOT: SchemaInventory = {
|
|
|
1979
1972
|
}
|
|
1980
1973
|
],
|
|
1981
1974
|
"sql":
|
|
1982
|
-
"CREATE TABLE runtime_sessions ( session_id TEXT PRIMARY KEY, conversation_id TEXT REFERENCES conversations(id), interface TEXT, mirror_active INTEGER NOT NULL DEFAULT 0, persona TEXT, journey TEXT,
|
|
1975
|
+
"CREATE TABLE runtime_sessions ( session_id TEXT PRIMARY KEY, conversation_id TEXT REFERENCES conversations(id), interface TEXT, mirror_active INTEGER NOT NULL DEFAULT 0, persona TEXT, journey TEXT, active INTEGER NOT NULL DEFAULT 1, started_at TEXT NOT NULL, updated_at TEXT NOT NULL, closed_at TEXT, metadata TEXT)"
|
|
1983
1976
|
},
|
|
1984
1977
|
"tasks": {
|
|
1985
1978
|
"columns": [
|
package/ts/src/db/schemaState.ts
CHANGED
|
@@ -47,6 +47,8 @@ export const KNOWN_MIGRATION_IDS: readonly string[] = [
|
|
|
47
47
|
"016_builder_workbench_display_codes",
|
|
48
48
|
// TS-authored, no Python counterpart (CV22.DS6.US2 — first TS ⊋ Python migration).
|
|
49
49
|
"017_journey_parent_column",
|
|
50
|
+
// CV9.DS8.US5: the inject hook's once-per-session flag, without a reader since US4.
|
|
51
|
+
"018_drop_hook_injected",
|
|
50
52
|
];
|
|
51
53
|
|
|
52
54
|
/**
|
|
@@ -17,6 +17,15 @@ const TS_ONLY_COLUMNS: Record<string, ColumnInventory[]> = {
|
|
|
17
17
|
identity: [{ name: "parent_journey", type: "TEXT", notnull: 0, dflt_value: null, pk: 0 }],
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Columns the Python snapshot has that TS has dropped (CV9.DS8.US5, migration
|
|
22
|
+
* 018): `runtime_sessions.hook_injected`, the inject hook's once-per-session
|
|
23
|
+
* flag, without a reader since CV9.DS8.US4.
|
|
24
|
+
*/
|
|
25
|
+
const TS_REMOVED_COLUMNS: Record<string, string[]> = {
|
|
26
|
+
runtime_sessions: ["hook_injected"],
|
|
27
|
+
};
|
|
28
|
+
|
|
20
29
|
/** Indexes TS creates that the Python snapshot does not have. */
|
|
21
30
|
const TS_ONLY_INDEXES: Record<string, { table: string; unique: number; columns: string[] }> = {
|
|
22
31
|
idx_identity_parent_journey: { table: "identity", unique: 0, columns: ["parent_journey"] },
|
|
@@ -46,21 +55,33 @@ export function diffTsInventoryAgainstSnapshot(
|
|
|
46
55
|
const actualTable = actual.tables[name];
|
|
47
56
|
if (!actualTable) continue;
|
|
48
57
|
const additions = TS_ONLY_COLUMNS[name] ?? [];
|
|
49
|
-
const
|
|
58
|
+
const removals = TS_REMOVED_COLUMNS[name] ?? [];
|
|
59
|
+
const expectedColumns = [
|
|
60
|
+
...snapshot.tables[name].columns.filter((column) => !removals.includes(column.name)),
|
|
61
|
+
...additions,
|
|
62
|
+
];
|
|
50
63
|
if (!isDeepStrictEqual(actualTable.columns, expectedColumns)) {
|
|
51
|
-
problems.push(
|
|
64
|
+
problems.push(
|
|
65
|
+
`tables[${name}].columns differ from snapshot + enumerated TS-only additions and removals`,
|
|
66
|
+
);
|
|
52
67
|
}
|
|
53
68
|
if (!isDeepStrictEqual(actualTable.foreign_keys, snapshot.tables[name].foreign_keys)) {
|
|
54
69
|
problems.push(`tables[${name}].foreign_keys differ from snapshot`);
|
|
55
70
|
}
|
|
56
|
-
if (additions.length > 0) {
|
|
71
|
+
if (additions.length > 0 || removals.length > 0) {
|
|
57
72
|
// The raw CREATE TABLE text diverges by exactly these columns; assert each
|
|
58
|
-
// added column name is present
|
|
73
|
+
// added column name is present and each removed one absent, rather than
|
|
74
|
+
// reproduce SQLite's stored text.
|
|
59
75
|
for (const column of additions) {
|
|
60
76
|
if (!(actualTable.sql ?? "").includes(column.name)) {
|
|
61
77
|
problems.push(`tables[${name}].sql is missing TS-only column ${column.name}`);
|
|
62
78
|
}
|
|
63
79
|
}
|
|
80
|
+
for (const column of removals) {
|
|
81
|
+
if ((actualTable.sql ?? "").includes(column)) {
|
|
82
|
+
problems.push(`tables[${name}].sql still carries the dropped column ${column}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
64
85
|
} else if (!isDeepStrictEqual(actualTable.sql, snapshot.tables[name].sql)) {
|
|
65
86
|
problems.push(`tables[${name}].sql differs from snapshot`);
|
|
66
87
|
}
|
package/ts/src/frontDoor/cli.ts
CHANGED
|
@@ -52,6 +52,7 @@ import { setIdentity } from "#identity/setIdentity.ts";
|
|
|
52
52
|
import { IdentityRootExistsError, initUserHome, TemplatesNotFoundError } from "#init/init.ts";
|
|
53
53
|
import { JOURNEY_PATH_LAYER } from "#journey/journeyStatus.ts";
|
|
54
54
|
import { JourneyNotFoundError } from "#journey/journeyWrite.ts";
|
|
55
|
+
import { environmentSessionId } from "#mirror/runtimeSession.ts";
|
|
55
56
|
import {
|
|
56
57
|
CallOutcomeTally,
|
|
57
58
|
formatCallOutcome,
|
|
@@ -116,9 +117,11 @@ import {
|
|
|
116
117
|
import { applyJourneySetPath } from "./journeyWriteRoute.ts";
|
|
117
118
|
import { openLiveWriteDatabase } from "./liveBackup.ts";
|
|
118
119
|
import {
|
|
120
|
+
isMirrorDoorman,
|
|
119
121
|
isMirrorWrite,
|
|
120
122
|
isModeWrite,
|
|
121
123
|
mirrorDeactivateMissingSession,
|
|
124
|
+
runMirrorDoormanRoute,
|
|
122
125
|
runMirrorRead,
|
|
123
126
|
runMirrorWriteRoute,
|
|
124
127
|
runModeRead,
|
|
@@ -172,6 +175,7 @@ import { runRuntimeReadRoute, runWelcomeRoute } from "./runtimeRoute.ts";
|
|
|
172
175
|
import { runMemorySearchRoute } from "./searchRoute.ts";
|
|
173
176
|
import { resolveSeedPaths } from "./seedPaths.ts";
|
|
174
177
|
import { isSessionStartWelcome, runSessionStartWelcomeRoute } from "./sessionStartWelcomeRoute.ts";
|
|
178
|
+
import { isSkillTurnsCommand, runSkillTurnsRoute } from "./skillTurnsRoute.ts";
|
|
175
179
|
import { runSoulRoute } from "./soulRoute.ts";
|
|
176
180
|
import {
|
|
177
181
|
applyTasksImport,
|
|
@@ -1476,7 +1480,10 @@ function runShadowWrite(argv: readonly string[]): number | Promise<number> {
|
|
|
1476
1480
|
return runShadowScanWrite(argv); // "scan"
|
|
1477
1481
|
}
|
|
1478
1482
|
|
|
1479
|
-
function withMirrorReadDb
|
|
1483
|
+
function withMirrorReadDb<T = number>(
|
|
1484
|
+
argv: readonly string[],
|
|
1485
|
+
read: (db: Database) => T,
|
|
1486
|
+
): T | number {
|
|
1480
1487
|
const dbPath = resolveDbPathForCli(argv.slice(1));
|
|
1481
1488
|
if (dbPath === null) return 2;
|
|
1482
1489
|
ensureDatabaseReadyForCli(dbPath, argv[0] ?? null);
|
|
@@ -1538,6 +1545,35 @@ function runModeWrite(argv: readonly string[]): Promise<number> {
|
|
|
1538
1545
|
return withMirrorWriteDb(argv, (db) => runModeWriteRoute(db, argv));
|
|
1539
1546
|
}
|
|
1540
1547
|
|
|
1548
|
+
/**
|
|
1549
|
+
* `mirror doorman` (CV9.DS8.TS2): decide on a read-only handle, write behind
|
|
1550
|
+
* the pre-write snapshot only for a turn that delivers. The memory scores a
|
|
1551
|
+
* delta turn ranks go to the front-door log as numbers -- content-free -- the
|
|
1552
|
+
* hybrid and the semantic the threshold reads (US5), every ranked memory's,
|
|
1553
|
+
* so the threshold is read against measurement.
|
|
1554
|
+
*/
|
|
1555
|
+
function runMirrorDoorman(argv: readonly string[]): Promise<number> {
|
|
1556
|
+
const reporter = outcomeReporter(argv, "reception");
|
|
1557
|
+
const logPath = resolveLogPath(argv);
|
|
1558
|
+
return runMirrorDoormanRoute(argv, {
|
|
1559
|
+
withReadOnlyDatabase: (read) => withMirrorReadDb(argv, read),
|
|
1560
|
+
withLiveWriteDatabase: (write) => withMirrorWriteDb(argv, write),
|
|
1561
|
+
onReceptionOutcome: reporter.onOutcome,
|
|
1562
|
+
onMemoryScores: (scores) =>
|
|
1563
|
+
logFrontDoor(logPath, {
|
|
1564
|
+
command: "mirror",
|
|
1565
|
+
route: "ts",
|
|
1566
|
+
exitCode: 0,
|
|
1567
|
+
detail:
|
|
1568
|
+
`doorman memory_scores=${scores.map((s) => s.score.toFixed(3)).join(",")}` +
|
|
1569
|
+
` memory_semantic=${scores.map((s) => s.semantic.toFixed(3)).join(",")}`,
|
|
1570
|
+
}),
|
|
1571
|
+
}).then((exitCode) => {
|
|
1572
|
+
if (reporter.calls > 0) reporter.logSummary();
|
|
1573
|
+
return exitCode;
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1541
1577
|
function runSoulWrite(argv: readonly string[]): Promise<number> {
|
|
1542
1578
|
return withMirrorWriteDb(argv, (db) => runSoulRoute(db, argv));
|
|
1543
1579
|
}
|
|
@@ -1738,7 +1774,11 @@ async function dispatchTs(argv: readonly string[]): Promise<number | DispatchOut
|
|
|
1738
1774
|
createLoadRuntime: (db, dbPath, ignoreProductionRole) =>
|
|
1739
1775
|
createBuildLoadRuntime({ db, dbPath, ignoreProductionRole }),
|
|
1740
1776
|
nowIso,
|
|
1741
|
-
|
|
1777
|
+
// The session the harness named -- MIRROR_SESSION_ID, else Claude Code's or
|
|
1778
|
+
// Codex's own -- the same reader the mirror route uses (CV9.DS8.US4: read
|
|
1779
|
+
// MIRROR_SESSION_ID alone, a Builder switch on Claude Code stamped a guessed
|
|
1780
|
+
// session and the doorman never saw it).
|
|
1781
|
+
environmentSessionId: environmentSessionId(process.env),
|
|
1742
1782
|
});
|
|
1743
1783
|
}
|
|
1744
1784
|
if (isInit(argv)) return runInit(argv);
|
|
@@ -1758,6 +1798,7 @@ async function dispatchTs(argv: readonly string[]): Promise<number | DispatchOut
|
|
|
1758
1798
|
if (argv[0] === "descriptor" && argv[1] === "generate") return runDescriptorContentRoute(argv);
|
|
1759
1799
|
if (isConsolidateSubcommandWrite(argv)) return runConsolidateWrite(argv);
|
|
1760
1800
|
if (isShadowSubcommandWrite(argv)) return runShadowWrite(argv);
|
|
1801
|
+
if (isMirrorDoorman(argv)) return runMirrorDoorman(argv);
|
|
1761
1802
|
if (isMirrorWrite(argv)) return runMirrorWrite(argv);
|
|
1762
1803
|
if (isModeWrite(argv)) return runModeWrite(argv);
|
|
1763
1804
|
// CV22.DS7.US6: the Soul ritual; `routing.ts` allowlists its subcommands by
|
|
@@ -1781,6 +1822,21 @@ async function dispatchTs(argv: readonly string[]): Promise<number | DispatchOut
|
|
|
1781
1822
|
verifyBackup: (archive) => verifyBackupArchive(archive),
|
|
1782
1823
|
});
|
|
1783
1824
|
}
|
|
1825
|
+
// CR160: the same shape -- the dry run reads; `--apply` writes behind the
|
|
1826
|
+
// pre-write backup, after its own dated, verified archive.
|
|
1827
|
+
if (isSkillTurnsCommand(argv)) {
|
|
1828
|
+
return runSkillTurnsRoute(argv, {
|
|
1829
|
+
withReadOnlyDatabase: (read) => withMirrorReadDb(argv, read),
|
|
1830
|
+
withLiveWriteDatabase: (write) => {
|
|
1831
|
+
const dbPath = resolveDbPathForCli(argv.slice(1));
|
|
1832
|
+
if (dbPath === null) return 2;
|
|
1833
|
+
return withLiveWriteDbAt(dbPath, "conversations", (db) => write(db, dbPath));
|
|
1834
|
+
},
|
|
1835
|
+
backup: (dbPath) =>
|
|
1836
|
+
createZipBackup({ dbPath, mirrorHome: dirname(dbPath), backupDir: null, silent: true }),
|
|
1837
|
+
verifyBackup: (archive) => verifyBackupArchive(archive),
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1784
1840
|
// CV22.DS7.TS1: the DB safety tools. `backup` never opens or bootstraps the
|
|
1785
1841
|
// database; `repair-encoding --apply` rides the live-write seam.
|
|
1786
1842
|
if (argv[0] === "backup") {
|
|
@@ -35,6 +35,24 @@ function subcommandOf(args: readonly string[]): string {
|
|
|
35
35
|
*/
|
|
36
36
|
const readStdin = takeStdin;
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* `--payload <json>`: a hook subcommand's payload in argv rather than on stdin
|
|
40
|
+
* (CR160). Pi's extension reaches the core through `pi.exec`, which cannot write
|
|
41
|
+
* a child's stdin, and since CR160 it sends Pi's turns to `user-prompt` and
|
|
42
|
+
* `stop`. Pi's prompt has travelled in argv since CV9, as `log-user`'s argument,
|
|
43
|
+
* so this is the exposure Pi always had, no wider (`stdinSource.ts`). The option
|
|
44
|
+
* is taken out of argv before the subcommand sees it, and a route given one
|
|
45
|
+
* never reads fd 0: a stdin pipe nobody closes cannot hang it.
|
|
46
|
+
*/
|
|
47
|
+
function takePayloadOption(args: readonly string[]): { payload: string | null; rest: string[] } {
|
|
48
|
+
const index = args.indexOf("--payload");
|
|
49
|
+
if (index === -1 || index + 1 >= args.length) return { payload: null, rest: [...args] };
|
|
50
|
+
return {
|
|
51
|
+
payload: args[index + 1] as string,
|
|
52
|
+
rest: [...args.slice(0, index), ...args.slice(index + 2)],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
/**
|
|
39
57
|
* Where the route's lines go. The default is the process's own streams; the
|
|
40
58
|
* `session-start --welcome` composite (CV9.E2.S32) captures them instead, so
|
|
@@ -51,7 +69,7 @@ export async function runConversationLoggerRoute(
|
|
|
51
69
|
argv: readonly string[],
|
|
52
70
|
io: ConversationLoggerRouteIo = {},
|
|
53
71
|
): Promise<number> {
|
|
54
|
-
const args = argv.slice(1);
|
|
72
|
+
const { payload, rest: args } = takePayloadOption(argv.slice(1));
|
|
55
73
|
const subcommand = subcommandOf(args);
|
|
56
74
|
const runtime = createLoggerRuntime({
|
|
57
75
|
db,
|
|
@@ -73,7 +91,7 @@ export async function runConversationLoggerRoute(
|
|
|
73
91
|
}),
|
|
74
92
|
});
|
|
75
93
|
const result = await runConversationLoggerCommand(db, args, runtime, {
|
|
76
|
-
stdin: STDIN_SUBCOMMANDS.has(subcommand) ? readStdin() : undefined,
|
|
94
|
+
stdin: STDIN_SUBCOMMANDS.has(subcommand) ? (payload ?? readStdin()) : undefined,
|
|
77
95
|
});
|
|
78
96
|
const out = io.stdout ?? ((line: string) => process.stdout.write(`${line}\n`));
|
|
79
97
|
const err = io.stderr ?? ((line: string) => process.stderr.write(`${line}\n`));
|
|
@@ -61,7 +61,7 @@ import { getIdentityContent } from "#identity/identityRead.ts";
|
|
|
61
61
|
import { getProjectPath } from "#journey/journeyStatus.ts";
|
|
62
62
|
import { loadMirrorContext } from "#mirror/context.ts";
|
|
63
63
|
import { persistStickyDefaults } from "#mirror/orchestration.ts";
|
|
64
|
-
import { resolveRuntimeSessionId } from "#mirror/runtimeSession.ts";
|
|
64
|
+
import { environmentSessionId, resolveRuntimeSessionId } from "#mirror/runtimeSession.ts";
|
|
65
65
|
import { activateOperatingMode, deactivateOperatingMode } from "#mode/operatingMode.ts";
|
|
66
66
|
import { newId, nowIso } from "#util/pyGenerators.ts";
|
|
67
67
|
import { optionValue, stripOptionWithValue } from "./args.ts";
|
|
@@ -430,7 +430,7 @@ async function runPromote(
|
|
|
430
430
|
// `--session-id`, which its promote parser does not even accept.
|
|
431
431
|
const result = await runBuildLoad(
|
|
432
432
|
db,
|
|
433
|
-
{ slug, environmentSessionId: process.env
|
|
433
|
+
{ slug, environmentSessionId: environmentSessionId(process.env) },
|
|
434
434
|
runtime.deps,
|
|
435
435
|
);
|
|
436
436
|
process.stderr.write(result.stderr);
|