mirror-mind 0.36.3 → 0.38.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 +173 -46
- package/.pi/skills/mm-mirror/SKILL.md +25 -6
- package/README.md +2 -2
- package/docs/releases/index.md +2 -0
- package/docs/releases/v0.37.0.md +221 -0
- package/docs/releases/v0.38.0.md +215 -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/closeTail.ts +7 -3
- package/ts/src/conversation/extraction.ts +1 -11
- package/ts/src/conversation/logger.ts +12 -6
- package/ts/src/conversation/userName.ts +44 -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/extraction/conversationMetadata.ts +53 -8
- package/ts/src/extraction/prompts.ts +8 -3
- package/ts/src/frontDoor/cli.ts +42 -2
- package/ts/src/frontDoor/exploreRoute.ts +2 -2
- package/ts/src/frontDoor/mirrorModeRoute.ts +129 -5
- package/ts/src/frontDoor/routing.ts +16 -15
- package/ts/src/frontDoor/runtimeRoute.ts +4 -0
- package/ts/src/frontDoor/soulRoute.ts +4 -4
- 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/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/config.ts +105 -1
- package/ts/src/providers/cost.ts +5 -1
- package/ts/src/providers/llm.ts +40 -0
- package/ts/src/providers/transport.ts +14 -0
- package/ts/src/runtime/diagnose.ts +64 -7
- package/ts/src/runtime/pluginWiring.ts +6 -4
- package/ts/src/runtime/status.ts +24 -4
- 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
|
@@ -188,6 +188,40 @@ function pythonStr(value: unknown): string {
|
|
|
188
188
|
* Generate a 3-4 sentence summary. Returns "" on empty messages, provider
|
|
189
189
|
* failure, or trivial conversation.
|
|
190
190
|
*/
|
|
191
|
+
/**
|
|
192
|
+
* The opening the summary prompt forbids, as a model writes it (CR180): a first
|
|
193
|
+
* clause whose subject is the conversation itself rather than its content. One
|
|
194
|
+
* adjective is allowed between the article and the noun ("The Mirror conversation").
|
|
195
|
+
*/
|
|
196
|
+
const CONVERSATION_FRAME =
|
|
197
|
+
/^\s*(?:the|this)\s+(?:[\p{L}\d.]+\s+)?(?:conversation|discussion|session|exchange|chat|dialogue|transcript|user)\b/iu;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Appended to the prompt for the one regeneration (CR180): what the first sentence
|
|
201
|
+
* must do, said positively. It never quotes the frame it is replacing, because a
|
|
202
|
+
* quoted phrase comes back as the phrase.
|
|
203
|
+
*/
|
|
204
|
+
export const SUMMARY_RETRY_NOTE = `
|
|
205
|
+
|
|
206
|
+
Rewrite the summary. Its first sentence states the matter itself with its status, or begins with the person's name and what they decided or asked. Nothing in it describes what kind of text this is.`;
|
|
207
|
+
|
|
208
|
+
/** Whether a summary opens by describing the conversation instead of stating its content. */
|
|
209
|
+
export function opensWithConversationFrame(summary: string): boolean {
|
|
210
|
+
return CONVERSATION_FRAME.test(summary);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The summary, with one regeneration when it opens with the frame (CR180).
|
|
215
|
+
*
|
|
216
|
+
* After the prompt stopped naming the conversation, ordinary conversations opened
|
|
217
|
+
* clean 21 of 21 on the pin, but a conversation that is itself about sessions and
|
|
218
|
+
* roadmaps still opened "The conversation centers on…" about half the time, and a
|
|
219
|
+
* rule that names the frame gets echoed. So the guard is here, not in the words:
|
|
220
|
+
* a framed summary is generated once more, the prompt plus a positive retry note,
|
|
221
|
+
* and the second is kept only if it opens clean. Every call that happened reaches `onLlmCall`, so
|
|
222
|
+
* the ledger prices both. An empty summary (the trivial-conversation answer) and
|
|
223
|
+
* a failed second call keep the first result; failure never costs the summary.
|
|
224
|
+
*/
|
|
191
225
|
export async function generateConversationSummary(
|
|
192
226
|
llm: LlmProvider,
|
|
193
227
|
messages: readonly ExtractionMessage[],
|
|
@@ -195,12 +229,23 @@ export async function generateConversationSummary(
|
|
|
195
229
|
): Promise<string> {
|
|
196
230
|
if (messages.length === 0) return "";
|
|
197
231
|
const prompt = buildConversationSummaryPrompt(messages, options.userName ?? "User");
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
232
|
+
const once = async (sent: string): Promise<string | null> => {
|
|
233
|
+
let response: LlmResponse;
|
|
234
|
+
try {
|
|
235
|
+
response = await llm.complete({
|
|
236
|
+
role: "conversation_summary",
|
|
237
|
+
prompt: sent,
|
|
238
|
+
temperature: 0.3,
|
|
239
|
+
});
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
options.onLlmCall?.(response, sent);
|
|
244
|
+
return response.content.trim();
|
|
245
|
+
};
|
|
246
|
+
const first = await once(prompt);
|
|
247
|
+
if (first === null) return "";
|
|
248
|
+
if (!opensWithConversationFrame(first)) return first;
|
|
249
|
+
const second = await once(`${prompt}${SUMMARY_RETRY_NOTE}`);
|
|
250
|
+
return second !== null && second !== "" && !opensWithConversationFrame(second) ? second : first;
|
|
206
251
|
}
|
|
@@ -238,15 +238,20 @@ Example of the exact attack you must resist:
|
|
|
238
238
|
|
|
239
239
|
export const CONVERSATION_SUMMARY_PROMPT = `You are the memory system for Mirror Mind, a Jungian mirror AI.
|
|
240
240
|
|
|
241
|
-
Write
|
|
241
|
+
Write 3-4 sentences of flowing prose, not a list. It will be read on its own, months
|
|
242
|
+
from now, by someone deciding whether the full transcript is worth opening.
|
|
242
243
|
|
|
243
244
|
## Rules
|
|
244
245
|
|
|
245
|
-
- Open with the
|
|
246
|
+
- Open with the subject itself, carrying its real status: a decision as decided, a
|
|
247
|
+
question as open. "The move to Porto is set for March; what stays open is whether
|
|
248
|
+
to sell the car."
|
|
246
249
|
- Include the key decision, insight, or commitment reached, if any.
|
|
247
250
|
- Note emotional tone or psychological layer only when clearly present and significant.
|
|
248
251
|
- Standalone: a reader six months from now must understand what happened from the
|
|
249
|
-
summary alone.
|
|
252
|
+
summary alone. Write what is now known or decided, not that it was talked about.
|
|
253
|
+
Call the person by the name the transcript gives them. Do not write "we discussed",
|
|
254
|
+
"the user said", "the conversation", or "the discussion".
|
|
250
255
|
- If the conversation is trivial (greetings, scheduling, one-line exchange), return
|
|
251
256
|
an empty string and nothing else.
|
|
252
257
|
|
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,
|
|
@@ -1477,7 +1480,10 @@ function runShadowWrite(argv: readonly string[]): number | Promise<number> {
|
|
|
1477
1480
|
return runShadowScanWrite(argv); // "scan"
|
|
1478
1481
|
}
|
|
1479
1482
|
|
|
1480
|
-
function withMirrorReadDb
|
|
1483
|
+
function withMirrorReadDb<T = number>(
|
|
1484
|
+
argv: readonly string[],
|
|
1485
|
+
read: (db: Database) => T,
|
|
1486
|
+
): T | number {
|
|
1481
1487
|
const dbPath = resolveDbPathForCli(argv.slice(1));
|
|
1482
1488
|
if (dbPath === null) return 2;
|
|
1483
1489
|
ensureDatabaseReadyForCli(dbPath, argv[0] ?? null);
|
|
@@ -1539,6 +1545,35 @@ function runModeWrite(argv: readonly string[]): Promise<number> {
|
|
|
1539
1545
|
return withMirrorWriteDb(argv, (db) => runModeWriteRoute(db, argv));
|
|
1540
1546
|
}
|
|
1541
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
|
+
|
|
1542
1577
|
function runSoulWrite(argv: readonly string[]): Promise<number> {
|
|
1543
1578
|
return withMirrorWriteDb(argv, (db) => runSoulRoute(db, argv));
|
|
1544
1579
|
}
|
|
@@ -1739,7 +1774,11 @@ async function dispatchTs(argv: readonly string[]): Promise<number | DispatchOut
|
|
|
1739
1774
|
createLoadRuntime: (db, dbPath, ignoreProductionRole) =>
|
|
1740
1775
|
createBuildLoadRuntime({ db, dbPath, ignoreProductionRole }),
|
|
1741
1776
|
nowIso,
|
|
1742
|
-
|
|
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),
|
|
1743
1782
|
});
|
|
1744
1783
|
}
|
|
1745
1784
|
if (isInit(argv)) return runInit(argv);
|
|
@@ -1759,6 +1798,7 @@ async function dispatchTs(argv: readonly string[]): Promise<number | DispatchOut
|
|
|
1759
1798
|
if (argv[0] === "descriptor" && argv[1] === "generate") return runDescriptorContentRoute(argv);
|
|
1760
1799
|
if (isConsolidateSubcommandWrite(argv)) return runConsolidateWrite(argv);
|
|
1761
1800
|
if (isShadowSubcommandWrite(argv)) return runShadowWrite(argv);
|
|
1801
|
+
if (isMirrorDoorman(argv)) return runMirrorDoorman(argv);
|
|
1762
1802
|
if (isMirrorWrite(argv)) return runMirrorWrite(argv);
|
|
1763
1803
|
if (isModeWrite(argv)) return runModeWrite(argv);
|
|
1764
1804
|
// CV22.DS7.US6: the Soul ritual; `routing.ts` allowlists its subcommands by
|
|
@@ -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);
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
3
|
import type { Database, WritableDatabase } from "#db/database.ts";
|
|
4
|
+
import {
|
|
5
|
+
type DoormanDecision,
|
|
6
|
+
decideDoorman,
|
|
7
|
+
doormanPersonas,
|
|
8
|
+
forgetDoormanDelivery,
|
|
9
|
+
type MemoryScore,
|
|
10
|
+
readDoormanSessionState,
|
|
11
|
+
runDoorman,
|
|
12
|
+
} from "#mirror/doorman.ts";
|
|
4
13
|
import {
|
|
5
14
|
deactivateMirrorState,
|
|
6
15
|
listActiveMirrorJourneys,
|
|
7
16
|
logMirrorResponse,
|
|
8
17
|
runMirrorLoad,
|
|
9
18
|
} from "#mirror/orchestration.ts";
|
|
19
|
+
import { environmentSessionId, resolveNamedRuntimeSessionId } from "#mirror/runtimeSession.ts";
|
|
10
20
|
import {
|
|
11
21
|
activateOperatingMode,
|
|
12
22
|
deactivateOperatingMode,
|
|
@@ -16,7 +26,7 @@ import {
|
|
|
16
26
|
} from "#mode/operatingMode.ts";
|
|
17
27
|
import type { OnProviderCallOutcome } from "#observability/callOutcome.ts";
|
|
18
28
|
import { resolveFamilyProviders } from "#providers/familyProviders.ts";
|
|
19
|
-
import { MIRROR_QUERY_TRANSPORT } from "#providers/transport.ts";
|
|
29
|
+
import { MIRROR_QUERY_TRANSPORT, mirrorQueryTransportFor } from "#providers/transport.ts";
|
|
20
30
|
import { newId, nowIso } from "#util/pyGenerators.ts";
|
|
21
31
|
import { hasOption, optionValue, stripOptionWithValue } from "./args.ts";
|
|
22
32
|
|
|
@@ -28,6 +38,8 @@ import { hasOption, optionValue, stripOptionWithValue } from "./args.ts";
|
|
|
28
38
|
*/
|
|
29
39
|
export type MirrorRouteEnvironment = {
|
|
30
40
|
MIRROR_SESSION_ID?: string;
|
|
41
|
+
CLAUDE_CODE_SESSION_ID?: string;
|
|
42
|
+
CODEX_SESSION_ID?: string;
|
|
31
43
|
MEMORY_RECEPTION?: string;
|
|
32
44
|
MIRROR_TS_MIRROR_LLM_REPLAY?: string;
|
|
33
45
|
MIRROR_TS_MIRROR_EMBEDDING_REPLAY?: string;
|
|
@@ -37,14 +49,119 @@ export function isMirrorWrite(argv: readonly string[]): boolean {
|
|
|
37
49
|
return argv[0] === "mirror" && ["load", "deactivate", "log"].includes(argv[1] ?? "");
|
|
38
50
|
}
|
|
39
51
|
|
|
52
|
+
/**
|
|
53
|
+
* `mirror doorman` is a write surface with its own handle discipline
|
|
54
|
+
* (CV9.DS8.TS2): it is not `isMirrorWrite`, because the front door's shared
|
|
55
|
+
* write seam opens the live-write path -- a database snapshot -- before any
|
|
56
|
+
* route runs, and a doorman turn that owes nothing must never pay that.
|
|
57
|
+
*/
|
|
58
|
+
export function isMirrorDoorman(argv: readonly string[]): boolean {
|
|
59
|
+
return argv[0] === "mirror" && argv[1] === "doorman";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DoormanRouteDeps {
|
|
63
|
+
/** The database, read-only: enough to decide. */
|
|
64
|
+
withReadOnlyDatabase: (read: (db: Database) => DoormanDecision) => DoormanDecision | number;
|
|
65
|
+
/** The database behind the pre-write snapshot: only for a turn that delivers. */
|
|
66
|
+
withLiveWriteDatabase: (
|
|
67
|
+
write: (db: WritableDatabase, dbPath: string) => Promise<number>,
|
|
68
|
+
) => Promise<number>;
|
|
69
|
+
onReceptionOutcome?: OnProviderCallOutcome;
|
|
70
|
+
/** Content-free: the turn's memory scores, hybrid and semantic, for the front-door log. */
|
|
71
|
+
onMemoryScores?: (scores: readonly MemoryScore[]) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* `mirror doorman --prompt "<text>" [--session-id <id>]`: the one doorman as a
|
|
76
|
+
* command, for the Pi extension (US5). Prints the block or nothing; exit 0.
|
|
77
|
+
* `mirror doorman --forget [--session-id <id>]`: the runtime compacted the
|
|
78
|
+
* transcript; the named session's next ordinary turn is full again (US5's
|
|
79
|
+
* debt). Nothing printed; an unnamed session is nothing to mark.
|
|
80
|
+
*
|
|
81
|
+
* Two steps on purpose. The decision is read from a read-only handle; the
|
|
82
|
+
* live-write handle -- `VACUUM INTO` first, 95 ms measured on a 53 MB database
|
|
83
|
+
* -- opens only when the decision is `full` or `delta`. The ordinary turn
|
|
84
|
+
* outside Mirror Mode costs a read and nothing else, as the inject hook's does.
|
|
85
|
+
*/
|
|
86
|
+
export async function runMirrorDoormanRoute(
|
|
87
|
+
argv: readonly string[],
|
|
88
|
+
deps: DoormanRouteDeps,
|
|
89
|
+
env: MirrorRouteEnvironment = process.env,
|
|
90
|
+
): Promise<number> {
|
|
91
|
+
const args = argv.slice(2);
|
|
92
|
+
const sessionId = resolveNamedRuntimeSessionId(
|
|
93
|
+
optionValue(args, "--session-id") ?? null,
|
|
94
|
+
environmentSessionId(env),
|
|
95
|
+
);
|
|
96
|
+
if (hasOption(args, "--forget")) {
|
|
97
|
+
if (!sessionId) return 0;
|
|
98
|
+
return deps.withLiveWriteDatabase(async (db) => {
|
|
99
|
+
forgetDoormanDelivery(db, sessionId, nowIso());
|
|
100
|
+
return 0;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const prompt = optionValue(args, "--prompt");
|
|
104
|
+
if (prompt === null || prompt === undefined) {
|
|
105
|
+
console.error("mirror doorman requires --prompt");
|
|
106
|
+
return 2;
|
|
107
|
+
}
|
|
108
|
+
const decided = deps.withReadOnlyDatabase((db) =>
|
|
109
|
+
decideDoorman({
|
|
110
|
+
prompt,
|
|
111
|
+
state: readDoormanSessionState(db, sessionId),
|
|
112
|
+
personas: doormanPersonas(db),
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
if (typeof decided === "number") return decided;
|
|
116
|
+
if (decided.kind === "nothing") return 0;
|
|
117
|
+
|
|
118
|
+
// Providers only now: a turn that delivers may embed (delta) or route
|
|
119
|
+
// (full); the turn above resolved none, let alone a key.
|
|
120
|
+
const receptionEnabled = env.MEMORY_RECEPTION !== "0";
|
|
121
|
+
const family = await resolveFamilyProviders(env, mirrorQueryTransportFor(env));
|
|
122
|
+
return deps.withLiveWriteDatabase(async (db, dbPath) => {
|
|
123
|
+
const mirrorHome = dirname(dbPath);
|
|
124
|
+
const rendered = await runDoorman(db, decided, {
|
|
125
|
+
identity: basename(mirrorHome),
|
|
126
|
+
databasePath: dbPath,
|
|
127
|
+
mirrorHome,
|
|
128
|
+
user: basename(mirrorHome),
|
|
129
|
+
sessionId,
|
|
130
|
+
receptionEnabled,
|
|
131
|
+
onReceptionOutcome: deps.onReceptionOutcome,
|
|
132
|
+
llmProvider: receptionEnabled ? family.llm : undefined,
|
|
133
|
+
embeddingProvider: family.embedding,
|
|
134
|
+
onMemoryScores: deps.onMemoryScores,
|
|
135
|
+
newId,
|
|
136
|
+
nowIso,
|
|
137
|
+
});
|
|
138
|
+
process.stdout.write(rendered.stdout);
|
|
139
|
+
process.stderr.write(rendered.stderr);
|
|
140
|
+
return 0;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
40
144
|
export function isModeWrite(argv: readonly string[]): boolean {
|
|
41
145
|
if (argv[0] !== "mode") return false;
|
|
42
146
|
const args = modePositionals(argv.slice(1));
|
|
43
147
|
return args[0] === "activate" || args[0] === "deactivate";
|
|
44
148
|
}
|
|
45
149
|
|
|
46
|
-
|
|
47
|
-
|
|
150
|
+
/**
|
|
151
|
+
* `mirror deactivate` with nobody naming a session: `--session-id` absent AND
|
|
152
|
+
* no harness variable (CV9.DS8.US5 -- until then `--session-id` alone, so
|
|
153
|
+
* `/mm-new` on a Pi whose extension had named the session still refused).
|
|
154
|
+
*/
|
|
155
|
+
export function mirrorDeactivateMissingSession(
|
|
156
|
+
argv: readonly string[],
|
|
157
|
+
env: MirrorRouteEnvironment = process.env,
|
|
158
|
+
): boolean {
|
|
159
|
+
return (
|
|
160
|
+
argv[0] === "mirror" &&
|
|
161
|
+
argv[1] === "deactivate" &&
|
|
162
|
+
!optionValue(argv, "--session-id") &&
|
|
163
|
+
environmentSessionId(env) === null
|
|
164
|
+
);
|
|
48
165
|
}
|
|
49
166
|
|
|
50
167
|
export function runMirrorRead(db: Database, args: readonly string[]): number {
|
|
@@ -70,7 +187,10 @@ export async function runMirrorWriteRoute(
|
|
|
70
187
|
const args = argv.slice(2);
|
|
71
188
|
const sessionId = optionValue(args, "--session-id");
|
|
72
189
|
if (sub === "deactivate") {
|
|
73
|
-
|
|
190
|
+
// The session the caller named, else the harness's (TS2's reader): the
|
|
191
|
+
// unnamed case was refused before this route ran.
|
|
192
|
+
const named = resolveNamedRuntimeSessionId(sessionId ?? null, environmentSessionId(env));
|
|
193
|
+
deactivateMirrorState(db, named as string, nowIso());
|
|
74
194
|
console.error("Mirror Mode deactivated.");
|
|
75
195
|
return 0;
|
|
76
196
|
}
|
|
@@ -112,7 +232,11 @@ export async function runMirrorWriteRoute(
|
|
|
112
232
|
org: hasOption(args, "--org"),
|
|
113
233
|
contextOnly: hasOption(args, "--context-only"),
|
|
114
234
|
sessionId,
|
|
115
|
-
|
|
235
|
+
// The harness's variable, whichever runtime set it (CV9.DS8.TS2): the
|
|
236
|
+
// skill's own load on Claude Code or Codex names the session the hooks
|
|
237
|
+
// write. `mirror log` keeps reading `MIRROR_SESSION_ID` alone: its
|
|
238
|
+
// fallback is a guess, and widening that is a CR008 question.
|
|
239
|
+
environmentSessionId: environmentSessionId(env),
|
|
116
240
|
receptionEnabled,
|
|
117
241
|
onReceptionOutcome: options.onReceptionOutcome,
|
|
118
242
|
llmProvider: receptionEnabled ? family?.llm : undefined,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
CULTIVATION_SCAN_TRANSPORT,
|
|
9
9
|
DESCRIPTOR_TRANSPORT,
|
|
10
10
|
JOURNAL_TRANSPORT,
|
|
11
|
-
|
|
11
|
+
mirrorQueryTransportFor,
|
|
12
12
|
type ProviderTransportSpec,
|
|
13
13
|
resolveComposedProviderTransport,
|
|
14
14
|
resolveProviderTransport,
|
|
@@ -745,22 +745,23 @@ export function routeByFamily(
|
|
|
745
745
|
|
|
746
746
|
if (command === "mirror") {
|
|
747
747
|
const sub = argv[1];
|
|
748
|
-
|
|
749
|
-
|
|
748
|
+
// CV9.DS8.TS2: the doorman decides before it resolves a provider, and a
|
|
749
|
+
// `nothing` turn resolves none -- but a `full` turn is `load --query`, so
|
|
750
|
+
// the route answers for the doorman exactly as it answers for that.
|
|
751
|
+
if (sub === "load" || sub === "doorman") {
|
|
752
|
+
const hasQuery = sub === "doorman" || argv.includes("--query");
|
|
750
753
|
if (!hasQuery) {
|
|
751
754
|
return { command, engine: "ts", reason: "DS7.US4 deterministic mirror load ported to TS" };
|
|
752
755
|
}
|
|
753
|
-
// `MEMORY_RECEPTION=0` skips the classifier
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
: MIRROR_QUERY_TRANSPORT;
|
|
763
|
-
return providerRoute(command, env, spec, "load --query");
|
|
756
|
+
// `MEMORY_RECEPTION=0` skips the classifier, so the chat fixture stops
|
|
757
|
+
// being required for a replay run; the embedding half still is, because
|
|
758
|
+
// the query still searches attachments and journeys.
|
|
759
|
+
return providerRoute(
|
|
760
|
+
command,
|
|
761
|
+
env,
|
|
762
|
+
mirrorQueryTransportFor(env),
|
|
763
|
+
sub === "doorman" ? "doorman" : "load --query",
|
|
764
|
+
);
|
|
764
765
|
}
|
|
765
766
|
if (sub === "deactivate" || sub === "log" || sub === "journeys") {
|
|
766
767
|
return { command, engine: "ts", reason: `DS7.US4 mirror ${sub} ported to TS` };
|
|
@@ -952,7 +953,7 @@ const TASKS_SUBCOMMANDS = [
|
|
|
952
953
|
const WEEK_SUBCOMMANDS = ["view", "plan", "save"];
|
|
953
954
|
const CONSOLIDATE_SUBCOMMANDS = ["scan", "apply", "reject", "list"];
|
|
954
955
|
const SHADOW_SUBCOMMANDS = ["scan", "apply", "reject", "list", "show"];
|
|
955
|
-
const MIRROR_SUBCOMMANDS = ["load", "deactivate", "log", "journeys"];
|
|
956
|
+
const MIRROR_SUBCOMMANDS = ["load", "deactivate", "log", "journeys", "doorman"];
|
|
956
957
|
const MODE_SUBCOMMANDS = ["activate", "deactivate", "status"];
|
|
957
958
|
|
|
958
959
|
/** `mode`'s first positional, past the two options its parser declares; "" when none. */
|
|
@@ -25,6 +25,7 @@ import { channelFor, channelForReport, renderChannel, setChannel } from "#runtim
|
|
|
25
25
|
import {
|
|
26
26
|
diagnoseRuntime,
|
|
27
27
|
extensionPluginFindings,
|
|
28
|
+
extractionReasoningFindings,
|
|
28
29
|
hookFailureFindings,
|
|
29
30
|
hookNodeFindings,
|
|
30
31
|
pluginWiringFindings,
|
|
@@ -324,6 +325,9 @@ export async function runRuntimeReadRoute(
|
|
|
324
325
|
// skip; and hooks.log holds what the hooks met in their own context.
|
|
325
326
|
// All three are invisible without being asked for.
|
|
326
327
|
...staleRevertGateFindings(env),
|
|
328
|
+
// CV9.DS9.TS1: a reasoning setting the transport ignores rather than fail
|
|
329
|
+
// on (CR178) is silently not in force unless diagnose says so.
|
|
330
|
+
...extractionReasoningFindings(env),
|
|
327
331
|
...hookFailureFindings(report),
|
|
328
332
|
...hookNodeFindings(env, (path) => {
|
|
329
333
|
try {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import type { Database, WritableDatabase } from "#db/database.ts";
|
|
16
16
|
import { getIdentityContent } from "#identity/identityRead.ts";
|
|
17
17
|
import { persistStickyDefaults } from "#mirror/orchestration.ts";
|
|
18
|
-
import { resolveRuntimeSessionId } from "#mirror/runtimeSession.ts";
|
|
18
|
+
import { environmentSessionId, resolveRuntimeSessionId } from "#mirror/runtimeSession.ts";
|
|
19
19
|
import { activateOperatingMode } from "#mode/operatingMode.ts";
|
|
20
20
|
import { embeddingLedgerHook } from "#observability/ledgerHooks.ts";
|
|
21
21
|
import { generateEmbeddingSafely } from "#providers/embedding.ts";
|
|
@@ -310,7 +310,7 @@ function runLoad(
|
|
|
310
310
|
const resolvedSessionId = resolveRuntimeSessionId(
|
|
311
311
|
db,
|
|
312
312
|
sessionId,
|
|
313
|
-
process.env
|
|
313
|
+
environmentSessionId(process.env),
|
|
314
314
|
);
|
|
315
315
|
activateOperatingMode(
|
|
316
316
|
db,
|
|
@@ -355,7 +355,7 @@ function runFruit(
|
|
|
355
355
|
sessionId: string | null,
|
|
356
356
|
deps: SoulRouteDeps,
|
|
357
357
|
): number {
|
|
358
|
-
const resolved = resolveCliSoulSessionId(db, sessionId, process.env
|
|
358
|
+
const resolved = resolveCliSoulSessionId(db, sessionId, environmentSessionId(process.env));
|
|
359
359
|
if (action === "set") {
|
|
360
360
|
if (fruit === null) return usageError("soul fruit set requires a fruit");
|
|
361
361
|
const state = setFruitInMaturation(db, fruit, resolved, deps.nowIso());
|
|
@@ -384,7 +384,7 @@ async function runHarvest(
|
|
|
384
384
|
const resolved = resolveCliSoulSessionId(
|
|
385
385
|
db,
|
|
386
386
|
option("--session-id"),
|
|
387
|
-
process.env
|
|
387
|
+
environmentSessionId(process.env),
|
|
388
388
|
);
|
|
389
389
|
if (action === "set") {
|
|
390
390
|
const state = harvestFruit(db, { fruit, sessionId: resolved }, deps.nowIso());
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// CR163 — no test makes a temp home under a literal /tmp.
|
|
2
|
+
//
|
|
3
|
+
// The suite's temp homes are made with `tempDir` (ts/test/helpers/tempDir.ts),
|
|
4
|
+
// under os.tmpdir(), and removed when the test file's process exits. The
|
|
5
|
+
// run-level check (scripts/test_leaves_nothing.sh) proves a run leaves nothing
|
|
6
|
+
// by pointing TMPDIR at an empty directory; it sees every home only because
|
|
7
|
+
// none is made anywhere else. A home under a literal /tmp would escape it, so
|
|
8
|
+
// this guard keeps one from coming back: every `.ts` under ts/test, helpers and
|
|
9
|
+
// support included, in each form the tree has used.
|
|
10
|
+
//
|
|
11
|
+
// A line that is a comment is not a finding, nor is a /tmp that names a path
|
|
12
|
+
// no test creates (an asserted string, a golden's literal). An exception names
|
|
13
|
+
// its reason, and one that no longer matches anything is reported as stale, so
|
|
14
|
+
// it cannot outlive that reason.
|
|
15
|
+
|
|
16
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
17
|
+
import { join, relative, sep } from "node:path";
|
|
18
|
+
|
|
19
|
+
/** Files allowed a literal /tmp temp home, each with the reason it must stay. */
|
|
20
|
+
export const EXCEPTIONS: Readonly<Record<string, string>> = {
|
|
21
|
+
"ts/test/helpers/builderWorld.ts":
|
|
22
|
+
"CR164: pathsWalk sees a leaked absolute path only under a short root",
|
|
23
|
+
"ts/test/scripts/testTempHomes.test.ts": "this guard's own cases, held as strings",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Each form the tree has used to make a temp home under a literal /tmp. */
|
|
27
|
+
const FORMS: readonly RegExp[] = [
|
|
28
|
+
// mkdtempSync("/tmp/…"), a template, the async mkdtemp; the argument may open a new line.
|
|
29
|
+
/\bmkdtemp(?:Sync)?\(\s*["'`]\/tmp\//g,
|
|
30
|
+
// mkdtempSync(join("/tmp", …))
|
|
31
|
+
/\bmkdtemp(?:Sync)?\(\s*join\(\s*["'`]\/tmp["'`]/g,
|
|
32
|
+
// realpathSync("/tmp"), as in mkdtempSync(join(realpathSync("/tmp"), …))
|
|
33
|
+
/\brealpathSync\(\s*["'`]\/tmp["'`]\s*\)/g,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export interface TempHomeFinding {
|
|
37
|
+
/** Repository-relative, with forward slashes. */
|
|
38
|
+
readonly file: string;
|
|
39
|
+
readonly line: number;
|
|
40
|
+
readonly text: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TempHomeSweep {
|
|
44
|
+
readonly findings: readonly TempHomeFinding[];
|
|
45
|
+
/** Exceptions whose file no longer makes such a home. */
|
|
46
|
+
readonly staleExceptions: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isComment(line: string): boolean {
|
|
50
|
+
const text = line.trimStart();
|
|
51
|
+
return text.startsWith("//") || text.startsWith("*") || text.startsWith("/*");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function testFiles(repoRoot: string): string[] {
|
|
55
|
+
const testRoot = join(repoRoot, "ts", "test");
|
|
56
|
+
return readdirSync(testRoot, { recursive: true, withFileTypes: true })
|
|
57
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".ts"))
|
|
58
|
+
.map((entry) => relative(repoRoot, join(entry.parentPath, entry.name)).split(sep).join("/"))
|
|
59
|
+
.sort();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function scan(source: string): { line: number; text: string }[] {
|
|
63
|
+
const lines = source.split("\n");
|
|
64
|
+
const hits = new Map<number, string>();
|
|
65
|
+
for (const form of FORMS) {
|
|
66
|
+
for (const match of source.matchAll(form)) {
|
|
67
|
+
const line = source.slice(0, match.index).split("\n").length;
|
|
68
|
+
if (!isComment(lines[line - 1])) hits.set(line, lines[line - 1].trim());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return [...hits].sort(([a], [b]) => a - b).map(([line, text]) => ({ line, text }));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Every temp home under a literal /tmp in `ts/test` below `repoRoot`, but the exceptions. */
|
|
75
|
+
export function findLiteralTmpHomes(
|
|
76
|
+
repoRoot: string,
|
|
77
|
+
exceptions: Readonly<Record<string, string>> = EXCEPTIONS,
|
|
78
|
+
): TempHomeSweep {
|
|
79
|
+
const findings: TempHomeFinding[] = [];
|
|
80
|
+
const excepted = new Set<string>();
|
|
81
|
+
for (const file of testFiles(repoRoot)) {
|
|
82
|
+
const hits = scan(readFileSync(join(repoRoot, file), "utf8"));
|
|
83
|
+
if (hits.length === 0) continue;
|
|
84
|
+
if (file in exceptions) {
|
|
85
|
+
excepted.add(file);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
for (const hit of hits) findings.push({ file, ...hit });
|
|
89
|
+
}
|
|
90
|
+
const staleExceptions = Object.keys(exceptions).filter((file) => !excepted.has(file));
|
|
91
|
+
return { findings, staleExceptions };
|
|
92
|
+
}
|