@geminixiang/mikan 1.0.0-beta.87 → 1.0.0-beta.89
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/CHANGELOG.md +44 -1370
- package/README.md +2 -2
- package/dist/adapters/commands/session-view.d.ts.map +1 -1
- package/dist/adapters/commands/session-view.js +4 -4
- package/dist/adapters/commands/session-view.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +4 -1
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +6 -3
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +3 -1
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/discord/types.d.ts +1 -0
- package/dist/adapters/discord/types.d.ts.map +1 -1
- package/dist/adapters/discord/types.js.map +1 -1
- package/dist/adapters/github/activity.d.ts.map +1 -1
- package/dist/adapters/github/activity.js +7 -13
- package/dist/adapters/github/activity.js.map +1 -1
- package/dist/adapters/github/bot.d.ts +4 -1
- package/dist/adapters/github/bot.d.ts.map +1 -1
- package/dist/adapters/github/bot.js +3 -3
- package/dist/adapters/github/bot.js.map +1 -1
- package/dist/adapters/github/context.d.ts.map +1 -1
- package/dist/adapters/github/context.js +3 -1
- package/dist/adapters/github/context.js.map +1 -1
- package/dist/adapters/github/types.d.ts +0 -3
- package/dist/adapters/github/types.d.ts.map +1 -1
- package/dist/adapters/github/types.js.map +1 -1
- package/dist/adapters/intake.d.ts.map +1 -1
- package/dist/adapters/intake.js +12 -2
- package/dist/adapters/intake.js.map +1 -1
- package/dist/adapters/shared.d.ts +2 -1
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/shared.js +7 -2
- package/dist/adapters/shared.js.map +1 -1
- package/dist/adapters/slack/assistant.d.ts +2 -4
- package/dist/adapters/slack/assistant.d.ts.map +1 -1
- package/dist/adapters/slack/assistant.js +10 -16
- package/dist/adapters/slack/assistant.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +5 -1
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +15 -11
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/response-lifecycle.d.ts.map +1 -1
- package/dist/adapters/slack/response-lifecycle.js +1 -1
- package/dist/adapters/slack/response-lifecycle.js.map +1 -1
- package/dist/adapters/slack/task-status.d.ts.map +1 -1
- package/dist/adapters/slack/task-status.js +1 -2
- package/dist/adapters/slack/task-status.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +4 -1
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +3 -3
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +3 -1
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/adapters/types.d.ts +3 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/adapters/types.js.map +1 -1
- package/dist/adapters/web/admin/client-assets.js +1 -1
- package/dist/adapters/web/admin/client-assets.js.map +1 -1
- package/dist/adapters/web/admin/portal.d.ts +0 -1
- package/dist/adapters/web/admin/portal.d.ts.map +1 -1
- package/dist/adapters/web/admin/portal.js +26 -42
- package/dist/adapters/web/admin/portal.js.map +1 -1
- package/dist/adapters/web/admin/types.d.ts +0 -2
- package/dist/adapters/web/admin/types.d.ts.map +1 -1
- package/dist/adapters/web/admin/types.js.map +1 -1
- package/dist/adapters/web/login/portal.d.ts.map +1 -1
- package/dist/adapters/web/login/portal.js +0 -8
- package/dist/adapters/web/login/portal.js.map +1 -1
- package/dist/adapters/web/server.d.ts.map +1 -1
- package/dist/adapters/web/server.js +0 -1
- package/dist/adapters/web/server.js.map +1 -1
- package/dist/adapters/web/session-view/portal.d.ts +1 -2
- package/dist/adapters/web/session-view/portal.d.ts.map +1 -1
- package/dist/adapters/web/session-view/portal.js +38 -328
- package/dist/adapters/web/session-view/portal.js.map +1 -1
- package/dist/adapters/web/session-view/types.d.ts +4 -6
- package/dist/adapters/web/session-view/types.d.ts.map +1 -1
- package/dist/adapters/web/session-view/types.js.map +1 -1
- package/dist/env-manifest.d.ts +0 -4
- package/dist/env-manifest.d.ts.map +1 -1
- package/dist/env-manifest.js +5 -8
- package/dist/env-manifest.js.map +1 -1
- package/dist/events/index.d.ts +2 -9
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +0 -1
- package/dist/events/index.js.map +1 -1
- package/dist/events/scheduler.d.ts +0 -1
- package/dist/events/scheduler.d.ts.map +1 -1
- package/dist/events/scheduler.js +2 -7
- package/dist/events/scheduler.js.map +1 -1
- package/dist/harness/execution-env.d.ts +1 -1
- package/dist/harness/execution-env.d.ts.map +1 -1
- package/dist/harness/execution-env.js +32 -32
- package/dist/harness/execution-env.js.map +1 -1
- package/dist/harness/index.d.ts +2 -3
- package/dist/harness/index.d.ts.map +1 -1
- package/dist/harness/index.js +0 -1
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/mcp-result.d.ts +4 -3
- package/dist/harness/mcp-result.d.ts.map +1 -1
- package/dist/harness/mcp-result.js +27 -13
- package/dist/harness/mcp-result.js.map +1 -1
- package/dist/harness/mcp.js +4 -4
- package/dist/harness/mcp.js.map +1 -1
- package/dist/harness/presenter.d.ts.map +1 -1
- package/dist/harness/presenter.js +20 -25
- package/dist/harness/presenter.js.map +1 -1
- package/dist/harness/prompt.d.ts +0 -1
- package/dist/harness/prompt.d.ts.map +1 -1
- package/dist/harness/prompt.js +3 -32
- package/dist/harness/prompt.js.map +1 -1
- package/dist/harness/run-events.d.ts.map +1 -1
- package/dist/harness/run-events.js +4 -12
- package/dist/harness/run-events.js.map +1 -1
- package/dist/harness/runner.d.ts.map +1 -1
- package/dist/harness/runner.js +35 -38
- package/dist/harness/runner.js.map +1 -1
- package/dist/harness/session.d.ts +37 -38
- package/dist/harness/session.d.ts.map +1 -1
- package/dist/harness/session.js +387 -388
- package/dist/harness/session.js.map +1 -1
- package/dist/harness/subagent.d.ts +5 -5
- package/dist/harness/subagent.d.ts.map +1 -1
- package/dist/harness/subagent.js +40 -45
- package/dist/harness/subagent.js.map +1 -1
- package/dist/harness/tools/codemode.d.ts +3 -0
- package/dist/harness/tools/codemode.d.ts.map +1 -0
- package/dist/harness/tools/codemode.js +173 -0
- package/dist/harness/tools/codemode.js.map +1 -0
- package/dist/harness/tools/history.d.ts +20 -0
- package/dist/harness/tools/history.d.ts.map +1 -0
- package/dist/harness/tools/history.js +293 -0
- package/dist/harness/tools/history.js.map +1 -0
- package/dist/harness/tools/pi-tools.d.ts +1 -1
- package/dist/harness/tools/pi-tools.d.ts.map +1 -1
- package/dist/harness/tools/pi-tools.js +47 -21
- package/dist/harness/tools/pi-tools.js.map +1 -1
- package/dist/harness/tools/secret-redaction.d.ts.map +1 -1
- package/dist/harness/tools/secret-redaction.js +46 -2
- package/dist/harness/tools/secret-redaction.js.map +1 -1
- package/dist/harness/tools/tool-search.d.ts +5 -0
- package/dist/harness/tools/tool-search.d.ts.map +1 -0
- package/dist/harness/tools/tool-search.js +84 -0
- package/dist/harness/tools/tool-search.js.map +1 -0
- package/dist/harness/types.d.ts +75 -15
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/index.d.ts +3 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +0 -3
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +0 -15
- package/dist/log.js.map +1 -1
- package/dist/main.js +2 -2
- package/dist/main.js.map +1 -1
- package/dist/memory-capture/index.js +0 -1
- package/dist/memory-capture/index.js.map +1 -1
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +2 -0
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/session-files.d.ts +3 -2
- package/dist/migrations/session-files.d.ts.map +1 -1
- package/dist/migrations/session-files.js.map +1 -1
- package/dist/migrations/session-v4.d.ts +77 -0
- package/dist/migrations/session-v4.d.ts.map +1 -0
- package/dist/migrations/session-v4.js +177 -0
- package/dist/migrations/session-v4.js.map +1 -0
- package/dist/migrations/sessions-sqlite.d.ts +3 -0
- package/dist/migrations/sessions-sqlite.d.ts.map +1 -0
- package/dist/migrations/sessions-sqlite.js +215 -0
- package/dist/migrations/sessions-sqlite.js.map +1 -0
- package/dist/migrations/sessions-v3.d.ts +2 -2
- package/dist/migrations/sessions-v3.d.ts.map +1 -1
- package/dist/migrations/sessions-v3.js +8 -19
- package/dist/migrations/sessions-v3.js.map +1 -1
- package/dist/migrations/types.d.ts +0 -5
- package/dist/migrations/types.d.ts.map +1 -1
- package/dist/migrations/types.js.map +1 -1
- package/dist/observability/sentry.d.ts +2 -9
- package/dist/observability/sentry.d.ts.map +1 -1
- package/dist/observability/sentry.js +12 -64
- package/dist/observability/sentry.js.map +1 -1
- package/dist/observability/types.d.ts +0 -16
- package/dist/observability/types.d.ts.map +1 -1
- package/dist/observability/types.js.map +1 -1
- package/dist/office/index.d.ts +1 -4
- package/dist/office/index.d.ts.map +1 -1
- package/dist/office/index.js +2 -14
- package/dist/office/index.js.map +1 -1
- package/dist/office/projection.d.ts.map +1 -1
- package/dist/office/projection.js +0 -1
- package/dist/office/projection.js.map +1 -1
- package/dist/office/types.d.ts +1 -2
- package/dist/office/types.d.ts.map +1 -1
- package/dist/office/types.js.map +1 -1
- package/dist/runtime/conversation-runtime.d.ts.map +1 -1
- package/dist/runtime/conversation-runtime.js +2 -16
- package/dist/runtime/conversation-runtime.js.map +1 -1
- package/dist/runtime/session-lifecycle.d.ts +1 -2
- package/dist/runtime/session-lifecycle.d.ts.map +1 -1
- package/dist/runtime/session-lifecycle.js +7 -16
- package/dist/runtime/session-lifecycle.js.map +1 -1
- package/dist/runtime/types.d.ts +0 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js.map +1 -1
- package/dist/sandbox/provisioner.d.ts.map +1 -1
- package/dist/sandbox/provisioner.js +3 -4
- package/dist/sandbox/provisioner.js.map +1 -1
- package/dist/sessions/chat-history-sync.d.ts +8 -6
- package/dist/sessions/chat-history-sync.d.ts.map +1 -1
- package/dist/sessions/chat-history-sync.js +70 -143
- package/dist/sessions/chat-history-sync.js.map +1 -1
- package/dist/sessions/compaction-summary.d.ts +4 -0
- package/dist/sessions/compaction-summary.d.ts.map +1 -0
- package/dist/sessions/compaction-summary.js +16 -0
- package/dist/sessions/compaction-summary.js.map +1 -0
- package/dist/sessions/session-key.d.ts +1 -0
- package/dist/sessions/session-key.d.ts.map +1 -1
- package/dist/sessions/session-key.js +3 -0
- package/dist/sessions/session-key.js.map +1 -1
- package/dist/sessions/session-store.d.ts +47 -29
- package/dist/sessions/session-store.d.ts.map +1 -1
- package/dist/sessions/session-store.js +578 -484
- package/dist/sessions/session-store.js.map +1 -1
- package/dist/sessions/types.d.ts +60 -46
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js +1 -1
- package/dist/sessions/types.js.map +1 -1
- package/dist/settings/apply.js +2 -2
- package/dist/settings/apply.js.map +1 -1
- package/dist/types.d.ts +10 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vault/index.d.ts.map +1 -1
- package/dist/vault/index.js +0 -3
- package/dist/vault/index.js.map +1 -1
- package/dist/vault/types.d.ts +0 -3
- package/dist/vault/types.d.ts.map +1 -1
- package/dist/vault/types.js.map +1 -1
- package/package.json +8 -5
- package/dist/sessions/store.d.ts +0 -17
- package/dist/sessions/store.d.ts.map +0 -1
- package/dist/sessions/store.js +0 -211
- package/dist/sessions/store.js.map +0 -1
|
@@ -1,303 +1,421 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
5
|
+
import { calculateContextTokens } from "@earendil-works/pi-ai/utils/estimate";
|
|
6
|
+
import { AssistantEntry, CompactionEntry, Harness, LiveDoc, MemoryStorage, ROOT_CONVERSATION_ID, ToolResultEntry, UserEntry, createRegistry, defineDoc, defineEntry, } from "@earendil-works/pi-durable";
|
|
7
|
+
import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
|
|
8
|
+
import { RUN_CAUSE_CUSTOM_TYPE } from "./types.js";
|
|
8
9
|
import { loadMcpTools, formatMcpServerInstructions } from "../harness/mcp.js";
|
|
9
10
|
import * as log from "../log.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
import { compactionSummaryOf, wrapCompactionSummary } from "./compaction-summary.js";
|
|
12
|
+
import { isThreadSessionKey } from "./session-key.js";
|
|
13
|
+
import { errorMessage, isRecord } from "../unknown-values.js";
|
|
14
|
+
const context = BACKGROUND_CONTEXT;
|
|
15
|
+
const ENTRY_PAGE_SIZE = 500;
|
|
16
|
+
const SessionDoc = defineDoc({
|
|
17
|
+
kind: "mikan.session",
|
|
18
|
+
version: 1,
|
|
19
|
+
scope: "conversation",
|
|
20
|
+
history: "latest",
|
|
21
|
+
fork: "initial",
|
|
22
|
+
initial: () => ({}),
|
|
23
|
+
});
|
|
24
|
+
const SessionIndexDoc = defineDoc({
|
|
25
|
+
kind: "mikan.sessions",
|
|
26
|
+
version: 1,
|
|
27
|
+
scope: "session",
|
|
28
|
+
initial: () => ({ sessions: {} }),
|
|
29
|
+
});
|
|
30
|
+
const CustomSessionEntry = defineEntry("mikan.custom");
|
|
31
|
+
function toJson(input) {
|
|
32
|
+
if (input === undefined)
|
|
33
|
+
return null;
|
|
34
|
+
const copy = JSON.parse(JSON.stringify(input));
|
|
35
|
+
return copy;
|
|
20
36
|
}
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const stats = statSync(path);
|
|
25
|
-
return `inode:${stats.dev}:${stats.ino}:${stats.birthtimeMs}`;
|
|
37
|
+
function durableMessage(message) {
|
|
38
|
+
const copy = JSON.parse(JSON.stringify(message));
|
|
39
|
+
return copy;
|
|
26
40
|
}
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
function messageTimestamp(message) {
|
|
42
|
+
return message && "timestamp" in message && typeof message.timestamp === "number"
|
|
43
|
+
? message.timestamp
|
|
44
|
+
: 0;
|
|
45
|
+
}
|
|
46
|
+
function contentText(message) {
|
|
47
|
+
if (!message || !("content" in message))
|
|
48
|
+
return "";
|
|
49
|
+
if (typeof message.content === "string")
|
|
50
|
+
return message.content;
|
|
51
|
+
return message.content
|
|
52
|
+
.map((part) => (part.type === "text" ? part.text : ""))
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.join("\n");
|
|
31
55
|
}
|
|
32
|
-
function
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
56
|
+
function toSessionEntry(record, sessionId) {
|
|
57
|
+
const id = `${sessionId}:${record.id}`;
|
|
58
|
+
const message = record.model?.[0];
|
|
59
|
+
if (UserEntry.is(record) || AssistantEntry.is(record) || ToolResultEntry.is(record)) {
|
|
60
|
+
if (!message)
|
|
61
|
+
return undefined;
|
|
62
|
+
return { type: "message", id, timestamp: messageTimestamp(message), message };
|
|
63
|
+
}
|
|
64
|
+
if (CompactionEntry.is(record)) {
|
|
65
|
+
return {
|
|
66
|
+
type: "compaction",
|
|
67
|
+
id,
|
|
68
|
+
timestamp: messageTimestamp(message),
|
|
69
|
+
summary: (message && compactionSummaryOf(message)) ?? contentText(message),
|
|
70
|
+
};
|
|
36
71
|
}
|
|
37
|
-
|
|
72
|
+
if (CustomSessionEntry.is(record)) {
|
|
73
|
+
return {
|
|
74
|
+
type: "custom",
|
|
75
|
+
id,
|
|
76
|
+
timestamp: record.data.timestamp,
|
|
77
|
+
customType: record.data.customType,
|
|
78
|
+
data: record.data.data ?? undefined,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
38
82
|
}
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
const key = sessionWriterKey(canonical);
|
|
42
|
-
claimWriter(key, canonical);
|
|
43
|
-
return { path: canonical, key };
|
|
83
|
+
function toSessionEntries(records, sessionId) {
|
|
84
|
+
return records.flatMap((record) => toSessionEntry(record, sessionId) ?? []);
|
|
44
85
|
}
|
|
45
|
-
function
|
|
46
|
-
|
|
86
|
+
async function readRecords(conversation) {
|
|
87
|
+
const records = [];
|
|
88
|
+
let cursor;
|
|
89
|
+
do {
|
|
90
|
+
const page = await conversation.entries({}, ENTRY_PAGE_SIZE, cursor, context);
|
|
91
|
+
records.push(...page.items);
|
|
92
|
+
cursor = page.next;
|
|
93
|
+
} while (cursor !== undefined);
|
|
94
|
+
return records.toReversed();
|
|
47
95
|
}
|
|
48
|
-
function
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return inodeKey;
|
|
96
|
+
async function readContext(conversation) {
|
|
97
|
+
const view = await conversation.context(context);
|
|
98
|
+
const messages = view.messages
|
|
99
|
+
.filter((message) => message.role !== "system")
|
|
100
|
+
.map(durableMessage);
|
|
101
|
+
return { messages };
|
|
55
102
|
}
|
|
56
|
-
function
|
|
57
|
-
|
|
103
|
+
function bindModels(models, property) {
|
|
104
|
+
const value = Reflect.get(models, property, models);
|
|
105
|
+
return typeof value === "function" ? value.bind(models) : value;
|
|
58
106
|
}
|
|
59
|
-
|
|
107
|
+
function requestOwnerKey(request) {
|
|
108
|
+
if (typeof request !== "object" || request === null || !("messages" in request))
|
|
109
|
+
return undefined;
|
|
110
|
+
const { messages } = request;
|
|
111
|
+
if (!Array.isArray(messages))
|
|
112
|
+
return undefined;
|
|
113
|
+
const last = messages.at(-1);
|
|
114
|
+
return typeof last === "object" && last !== null ? last : undefined;
|
|
60
115
|
}
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const chunks = [];
|
|
66
|
-
let total = 0;
|
|
67
|
-
try {
|
|
68
|
-
while (total < MAX_HEADER_BYTES) {
|
|
69
|
-
const chunk = Buffer.allocUnsafe(Math.min(HEADER_CHUNK_SIZE, MAX_HEADER_BYTES - total));
|
|
70
|
-
const bytesRead = readSync(fd, chunk, 0, chunk.length, null);
|
|
71
|
-
if (bytesRead === 0)
|
|
72
|
-
break;
|
|
73
|
-
const data = chunk.subarray(0, bytesRead);
|
|
74
|
-
const newline = data.indexOf(0x0a);
|
|
75
|
-
chunks.push(newline < 0 ? data : data.subarray(0, newline));
|
|
76
|
-
total += newline < 0 ? bytesRead : newline;
|
|
77
|
-
if (newline >= 0)
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
116
|
+
function protectStorageFiles(path) {
|
|
117
|
+
for (const file of [path, `${path}-wal`, `${path}-shm`]) {
|
|
118
|
+
if (existsSync(file))
|
|
119
|
+
chmodSync(file, 0o600);
|
|
80
120
|
}
|
|
81
|
-
finally {
|
|
82
|
-
closeSync(fd);
|
|
83
|
-
}
|
|
84
|
-
if (total >= MAX_HEADER_BYTES) {
|
|
85
|
-
throw new SessionFormatError(`Session file header exceeds ${MAX_HEADER_BYTES} bytes: ${filePath}`);
|
|
86
|
-
}
|
|
87
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
88
121
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
122
|
+
const REQUEST_METHODS = new Set(["streamSimple", "completeSimple"]);
|
|
123
|
+
class OfficeStorage {
|
|
124
|
+
static { this.pool = new Map(); }
|
|
125
|
+
constructor(path) {
|
|
126
|
+
this.path = path;
|
|
127
|
+
this.registry = createRegistry();
|
|
128
|
+
this.refs = 0;
|
|
129
|
+
this.bindings = new Map();
|
|
130
|
+
this.owners = new WeakMap();
|
|
131
|
+
this.writers = new Set();
|
|
132
|
+
}
|
|
133
|
+
get harness() {
|
|
134
|
+
if (!this.opened)
|
|
135
|
+
throw new Error("Office session storage is not open");
|
|
136
|
+
return this.opened;
|
|
137
|
+
}
|
|
138
|
+
static async acquire(path) {
|
|
139
|
+
let pending = OfficeStorage.pool.get(path);
|
|
140
|
+
if (!pending) {
|
|
141
|
+
pending = OfficeStorage.openShared(path);
|
|
142
|
+
OfficeStorage.pool.set(path, pending);
|
|
143
|
+
pending.catch(() => OfficeStorage.pool.delete(path));
|
|
144
|
+
}
|
|
145
|
+
const storage = await pending;
|
|
146
|
+
storage.refs += 1;
|
|
147
|
+
return storage;
|
|
148
|
+
}
|
|
149
|
+
static async inMemory() {
|
|
150
|
+
const storage = await OfficeStorage.open(null, new MemoryStorage());
|
|
151
|
+
storage.refs = 1;
|
|
152
|
+
return storage;
|
|
153
|
+
}
|
|
154
|
+
static async openShared(path) {
|
|
155
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
156
|
+
const storage = await OfficeStorage.open(path, await openNodeSqliteStorage(path));
|
|
157
|
+
protectStorageFiles(path);
|
|
158
|
+
await storage.abortUnfinishedWork();
|
|
159
|
+
return storage;
|
|
160
|
+
}
|
|
161
|
+
static async open(path, backend) {
|
|
162
|
+
const storage = new OfficeStorage(path);
|
|
163
|
+
storage.opened = await Harness.open(backend, storage.harnessOptions(), context);
|
|
164
|
+
return storage;
|
|
165
|
+
}
|
|
166
|
+
harnessOptions() {
|
|
167
|
+
const latest = () => [...this.bindings.values()].at(-1);
|
|
168
|
+
const fallback = () => {
|
|
169
|
+
const models = latest()?.models;
|
|
170
|
+
if (!models)
|
|
171
|
+
throw new Error("Session has no model binding; start a run to use the model");
|
|
172
|
+
return models;
|
|
173
|
+
};
|
|
174
|
+
const models = new Proxy({}, {
|
|
175
|
+
get: (_target, property) => {
|
|
176
|
+
if (!REQUEST_METHODS.has(property))
|
|
177
|
+
return bindModels(fallback(), property);
|
|
178
|
+
return (...args) => {
|
|
179
|
+
const key = requestOwnerKey(args[1]);
|
|
180
|
+
const owner = key ? this.owners.get(key) : undefined;
|
|
181
|
+
const binding = owner === undefined ? undefined : this.bindings.get(owner);
|
|
182
|
+
const target = binding?.requestModels ?? binding?.models ?? fallback();
|
|
183
|
+
const method = Reflect.get(target, property, target);
|
|
184
|
+
return method.apply(target, args);
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
const settings = () => latest()?.settings;
|
|
189
|
+
return {
|
|
190
|
+
models,
|
|
191
|
+
registry: this.registry,
|
|
192
|
+
settings: {
|
|
193
|
+
extensions: [],
|
|
194
|
+
get stream() {
|
|
195
|
+
return settings()?.stream;
|
|
196
|
+
},
|
|
197
|
+
get retry() {
|
|
198
|
+
return settings()?.retry;
|
|
199
|
+
},
|
|
200
|
+
get compaction() {
|
|
201
|
+
return settings()?.compaction;
|
|
202
|
+
},
|
|
203
|
+
get toolExecution() {
|
|
204
|
+
return settings()?.toolExecution;
|
|
205
|
+
},
|
|
206
|
+
get steeringMode() {
|
|
207
|
+
return settings()?.steeringMode;
|
|
208
|
+
},
|
|
209
|
+
get followUpMode() {
|
|
210
|
+
return settings()?.followUpMode;
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
env: (target, envContext) => this.bindings.get(target.conversationId)?.env?.(target, envContext),
|
|
214
|
+
onReport: (error) => {
|
|
215
|
+
const report = latest()?.onReport;
|
|
216
|
+
if (report)
|
|
217
|
+
report(error);
|
|
218
|
+
else
|
|
219
|
+
log.logWarning("Durable harness report", errorMessage(error));
|
|
220
|
+
},
|
|
221
|
+
};
|
|
96
222
|
}
|
|
97
|
-
|
|
98
|
-
|
|
223
|
+
async abortUnfinishedWork() {
|
|
224
|
+
const { tasks } = await this.harness.inspect(context);
|
|
225
|
+
const unowned = tasks.filter((task) => !task.record.owner);
|
|
226
|
+
if (unowned.length === 0)
|
|
227
|
+
return;
|
|
228
|
+
log.logWarning(`Aborting ${unowned.length} durable tasks left unfinished by a previous process`);
|
|
229
|
+
for (const task of unowned)
|
|
230
|
+
await this.harness.abortTask(task.record.id, context);
|
|
231
|
+
}
|
|
232
|
+
async index() {
|
|
233
|
+
return (await this.harness.snapshot(SessionIndexDoc, context))?.sessions ?? {};
|
|
234
|
+
}
|
|
235
|
+
claimWriter(conversationId, key) {
|
|
236
|
+
if (this.writers.has(conversationId)) {
|
|
237
|
+
throw new Error(`Session already has an active writer: ${key}`);
|
|
238
|
+
}
|
|
239
|
+
this.writers.add(conversationId);
|
|
99
240
|
}
|
|
100
|
-
|
|
101
|
-
|
|
241
|
+
releaseWriter(conversationId) {
|
|
242
|
+
this.writers.delete(conversationId);
|
|
102
243
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
244
|
+
bind(conversationId, binding) {
|
|
245
|
+
if (this.bindings.has(conversationId)) {
|
|
246
|
+
throw new Error("Session already has an attached harness");
|
|
247
|
+
}
|
|
248
|
+
this.bindings.set(conversationId, binding);
|
|
249
|
+
}
|
|
250
|
+
unbind(conversationId) {
|
|
251
|
+
this.bindings.delete(conversationId);
|
|
252
|
+
}
|
|
253
|
+
tag(conversationId, messages) {
|
|
254
|
+
const last = messages.at(-1);
|
|
255
|
+
if (!last)
|
|
256
|
+
return messages;
|
|
257
|
+
const tagged = { ...last };
|
|
258
|
+
this.owners.set(tagged, conversationId);
|
|
259
|
+
return [...messages.slice(0, -1), tagged];
|
|
260
|
+
}
|
|
261
|
+
async release() {
|
|
262
|
+
this.refs -= 1;
|
|
263
|
+
if (this.refs > 0)
|
|
264
|
+
return;
|
|
265
|
+
if (this.path !== null)
|
|
266
|
+
OfficeStorage.pool.delete(this.path);
|
|
267
|
+
await this.harness.close(context);
|
|
111
268
|
}
|
|
112
|
-
return parsed;
|
|
113
269
|
}
|
|
114
|
-
function
|
|
115
|
-
return {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
270
|
+
function writeSessionIndex(tx, key, record) {
|
|
271
|
+
return tx.doc(SessionIndexDoc).then((doc) => {
|
|
272
|
+
doc.sessions[key] = record;
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
async function lookupSession(storage, key) {
|
|
276
|
+
const record = (await storage.index())[key];
|
|
277
|
+
if (!record)
|
|
278
|
+
return undefined;
|
|
279
|
+
const conversation = await storage.harness.conversation(record.conversationId, context);
|
|
280
|
+
return conversation ? { conversation, record } : undefined;
|
|
281
|
+
}
|
|
282
|
+
async function createSession(storage, key) {
|
|
283
|
+
let record;
|
|
284
|
+
const init = async (tx, conversationId) => {
|
|
285
|
+
record = { conversationId, id: randomUUID(), createdAt: Date.now() };
|
|
286
|
+
await writeSessionIndex(tx, key, record);
|
|
124
287
|
};
|
|
288
|
+
const conversation = isThreadSessionKey(key)
|
|
289
|
+
? await storage.harness.createConversation({ ownership: { kind: "ownerless" }, init }, context)
|
|
290
|
+
: await storage.harness.root(context, { init });
|
|
291
|
+
if (!record) {
|
|
292
|
+
record = { conversationId: conversation.id, id: randomUUID(), createdAt: Date.now() };
|
|
293
|
+
const created = record;
|
|
294
|
+
await conversation.commit((tx) => writeSessionIndex(tx, key, created), context);
|
|
295
|
+
}
|
|
296
|
+
return { conversation, record };
|
|
125
297
|
}
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
298
|
+
async function parentSessionId(storage, conversationId) {
|
|
299
|
+
const parent = await storage.harness.commit(async (tx) => (await tx.conversation(conversationId))?.parent, context);
|
|
300
|
+
if (!parent)
|
|
301
|
+
return undefined;
|
|
302
|
+
const records = Object.values(await storage.index());
|
|
303
|
+
return records.find((record) => record.conversationId === parent.conversationId)?.id;
|
|
304
|
+
}
|
|
305
|
+
function isRunCause(record, messageId) {
|
|
306
|
+
if (!CustomSessionEntry.is(record) || record.data.customType !== RUN_CAUSE_CUSTOM_TYPE) {
|
|
307
|
+
return false;
|
|
135
308
|
}
|
|
136
|
-
|
|
309
|
+
const { data } = record.data;
|
|
310
|
+
return messageId === undefined || (isRecord(data) && data.messageId === messageId);
|
|
137
311
|
}
|
|
138
|
-
function
|
|
139
|
-
|
|
312
|
+
function runEnd(records, messageId) {
|
|
313
|
+
const start = records.findLastIndex((record) => isRunCause(record, messageId));
|
|
314
|
+
if (start < 0)
|
|
315
|
+
return undefined;
|
|
316
|
+
const next = records.findIndex((record, index) => index > start && isRunCause(record));
|
|
317
|
+
const run = records.slice(start, next < 0 ? undefined : next);
|
|
318
|
+
return (run.findLast((record) => AssistantEntry.is(record)) ?? run.at(-1))?.id;
|
|
140
319
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
transaction = JSON.parse(line);
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
return [];
|
|
148
|
-
}
|
|
149
|
-
const writes = Array.isArray(transaction) ? transaction : [transaction];
|
|
150
|
-
return writes.filter((write) => isMetadataRecord(write) &&
|
|
151
|
-
write.kind === "value" &&
|
|
152
|
-
write.namespace === "mikan" &&
|
|
153
|
-
write.key === "metadata");
|
|
320
|
+
const EARLIER_SESSION_PREFIX = "earlier:";
|
|
321
|
+
export function earlierSessionKey(sessionId) {
|
|
322
|
+
return `${EARLIER_SESSION_PREFIX}${sessionId}`;
|
|
154
323
|
}
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
const stats = fstatSync(fd);
|
|
159
|
-
return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}`;
|
|
160
|
-
}
|
|
161
|
-
finally {
|
|
162
|
-
closeSync(fd);
|
|
163
|
-
}
|
|
324
|
+
export function isEarlierSessionKey(key) {
|
|
325
|
+
return key.startsWith(EARLIER_SESSION_PREFIX);
|
|
164
326
|
}
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
327
|
+
async function appendImportedEntry(tx, conversationId, entry) {
|
|
328
|
+
switch (entry.type) {
|
|
329
|
+
case "message": {
|
|
330
|
+
const model = [durableMessage(entry.message)];
|
|
331
|
+
if (entry.message.role === "user")
|
|
332
|
+
await tx.appendEntry(UserEntry, conversationId, { model });
|
|
333
|
+
else if (entry.message.role === "assistant") {
|
|
334
|
+
await tx.appendEntry(AssistantEntry, conversationId, { model });
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
await tx.appendEntry(ToolResultEntry, conversationId, {
|
|
338
|
+
model,
|
|
339
|
+
data: { diagnostics: [] },
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
175
343
|
}
|
|
176
|
-
|
|
344
|
+
case "compaction":
|
|
345
|
+
await tx.appendEntry(CompactionEntry, conversationId, {
|
|
346
|
+
model: [
|
|
347
|
+
{
|
|
348
|
+
role: "user",
|
|
349
|
+
content: [{ type: "text", text: wrapCompactionSummary(entry.summary) }],
|
|
350
|
+
timestamp: entry.timestamp,
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
data: { reason: "manual" },
|
|
354
|
+
head: "self",
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
case "custom":
|
|
358
|
+
await tx.appendEntry(CustomSessionEntry, conversationId, {
|
|
359
|
+
data: {
|
|
360
|
+
customType: entry.customType,
|
|
361
|
+
data: toJson(entry.data),
|
|
362
|
+
timestamp: entry.timestamp,
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
return;
|
|
366
|
+
default:
|
|
367
|
+
entry;
|
|
177
368
|
}
|
|
178
|
-
|
|
369
|
+
}
|
|
370
|
+
export async function importOfficeSessions(path, sessions) {
|
|
371
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
372
|
+
const harness = await Harness.open(await openNodeSqliteStorage(path), { models: {}, registry: createRegistry(), settings: { extensions: [] } }, context);
|
|
179
373
|
try {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
374
|
+
for (const session of sessions) {
|
|
375
|
+
const init = async (tx, conversationId) => {
|
|
376
|
+
for (const entry of session.entries)
|
|
377
|
+
await appendImportedEntry(tx, conversationId, entry);
|
|
378
|
+
if (session.name)
|
|
379
|
+
(await tx.doc(SessionDoc, conversationId)).name = session.name;
|
|
380
|
+
await writeSessionIndex(tx, session.key, {
|
|
381
|
+
conversationId,
|
|
382
|
+
id: session.id,
|
|
383
|
+
createdAt: session.createdAt,
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
if (session.root)
|
|
387
|
+
await harness.root(context, { init });
|
|
388
|
+
else
|
|
389
|
+
await harness.createConversation({ ownership: { kind: "ownerless" }, init }, context);
|
|
184
390
|
}
|
|
185
|
-
ftruncateSync(fd, 0);
|
|
186
|
-
writeFileSync(fd, content);
|
|
187
391
|
}
|
|
188
392
|
finally {
|
|
189
|
-
|
|
393
|
+
await harness.close(context);
|
|
190
394
|
}
|
|
395
|
+
protectStorageFiles(path);
|
|
191
396
|
}
|
|
192
|
-
function
|
|
193
|
-
const
|
|
194
|
-
? undefined
|
|
195
|
-
: { parentSessionPath: options.parentSession };
|
|
196
|
-
return {
|
|
197
|
-
header: {
|
|
198
|
-
v: CURRENT_SESSION_VERSION,
|
|
199
|
-
kind: "header",
|
|
200
|
-
id: options?.id ?? uuidv7(),
|
|
201
|
-
storageVersion: JSONL_STORAGE_VERSION,
|
|
202
|
-
createdAt: Date.now(),
|
|
203
|
-
cwd,
|
|
204
|
-
parentSessionId: options?.parentSessionId,
|
|
205
|
-
legacyParentSessionPath: options?.parentSession,
|
|
206
|
-
},
|
|
207
|
-
metadata,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
function headerView(header, metadata) {
|
|
211
|
-
const parentSession = typeof metadata?.parentSessionPath === "string"
|
|
212
|
-
? metadata.parentSessionPath
|
|
213
|
-
: header.legacyParentSessionPath;
|
|
214
|
-
return {
|
|
215
|
-
type: "session",
|
|
216
|
-
version: CURRENT_SESSION_VERSION,
|
|
217
|
-
id: header.id,
|
|
218
|
-
timestamp: new Date(header.createdAt).toISOString(),
|
|
219
|
-
cwd: header.cwd,
|
|
220
|
-
parentSessionId: header.parentSessionId,
|
|
221
|
-
parentSession,
|
|
222
|
-
metadata,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
async function openFileSession(path, header) {
|
|
226
|
-
const sessionRepo = new JsonlSessionRepo({
|
|
227
|
-
fileSystem: new NodeExecutionEnv({ cwd: process.cwd() }),
|
|
228
|
-
sessionsRoot: dirname(path),
|
|
229
|
-
});
|
|
230
|
-
try {
|
|
231
|
-
const session = await sessionRepo.open(metadataFromHeader(header, path), TODO_CONTEXT);
|
|
232
|
-
return { session, repo: sessionRepo };
|
|
233
|
-
}
|
|
234
|
-
catch (error) {
|
|
235
|
-
await sessionRepo.close(TODO_CONTEXT);
|
|
236
|
-
throw error;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
async function withSessionSnapshot(path, read) {
|
|
240
|
-
const resolvedPath = canonicalSessionPath(path);
|
|
241
|
-
const dir = mkdtempSync(join(tmpdir(), "mikan-session-inspect-"));
|
|
242
|
-
const snapshot = join(dir, basename(resolvedPath));
|
|
243
|
-
let opened;
|
|
397
|
+
export async function readImportedContexts(path) {
|
|
398
|
+
const harness = await Harness.open(await openNodeSqliteStorage(path), { models: {}, registry: createRegistry(), settings: { extensions: [] } }, context);
|
|
244
399
|
try {
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
try {
|
|
252
|
-
await opened?.session.close(TODO_CONTEXT);
|
|
253
|
-
}
|
|
254
|
-
finally {
|
|
255
|
-
try {
|
|
256
|
-
await opened?.repo.close(TODO_CONTEXT);
|
|
257
|
-
}
|
|
258
|
-
finally {
|
|
259
|
-
rmSync(dir, { recursive: true, force: true });
|
|
260
|
-
}
|
|
400
|
+
const index = (await harness.snapshot(SessionIndexDoc, context))?.sessions ?? {};
|
|
401
|
+
const contexts = new Map();
|
|
402
|
+
for (const [key, record] of Object.entries(index)) {
|
|
403
|
+
const conversation = await harness.conversation(record.conversationId, context);
|
|
404
|
+
if (conversation)
|
|
405
|
+
contexts.set(key, (await readContext(conversation)).messages);
|
|
261
406
|
}
|
|
407
|
+
return contexts;
|
|
262
408
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const existing = await session.branch("main", TODO_CONTEXT);
|
|
266
|
-
if (existing || !create)
|
|
267
|
-
return existing;
|
|
268
|
-
return session.createBranch("main", null, TODO_CONTEXT);
|
|
269
|
-
}
|
|
270
|
-
function isContextMessage(message) {
|
|
271
|
-
return (message.role !== "assistant" ||
|
|
272
|
-
(message.stopReason !== "error" &&
|
|
273
|
-
message.stopReason !== "aborted" &&
|
|
274
|
-
message.stopReason !== "deferred"));
|
|
275
|
-
}
|
|
276
|
-
function buildContext(entries) {
|
|
277
|
-
const compactionIndex = entries.findLastIndex((entry) => entry.type === "compaction");
|
|
278
|
-
const visibleEntries = entries.slice(Math.max(0, compactionIndex));
|
|
279
|
-
const messages = [];
|
|
280
|
-
for (const entry of visibleEntries) {
|
|
281
|
-
if (entry.type === "message") {
|
|
282
|
-
if (isContextMessage(entry.message))
|
|
283
|
-
messages.push(entry.message);
|
|
284
|
-
}
|
|
285
|
-
else if (entry.type === "compaction") {
|
|
286
|
-
messages.push(createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp), ...entry.retainedTail.filter(isContextMessage));
|
|
287
|
-
}
|
|
288
|
-
else if (entry.type === "branch_summary" && entry.summary) {
|
|
289
|
-
messages.push(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp));
|
|
290
|
-
}
|
|
409
|
+
finally {
|
|
410
|
+
await harness.close(context);
|
|
291
411
|
}
|
|
292
|
-
return { messages };
|
|
293
412
|
}
|
|
294
413
|
class CachedSessionInspection {
|
|
295
|
-
constructor(header, entries, name,
|
|
414
|
+
constructor(header, entries, name, sessionContext) {
|
|
296
415
|
this.header = header;
|
|
297
416
|
this.entries = entries;
|
|
298
417
|
this.name = name;
|
|
299
|
-
this.
|
|
300
|
-
this.context = context;
|
|
418
|
+
this.sessionContext = sessionContext;
|
|
301
419
|
}
|
|
302
420
|
getHeader() {
|
|
303
421
|
return structuredClone(this.header);
|
|
@@ -308,190 +426,173 @@ class CachedSessionInspection {
|
|
|
308
426
|
async getSessionName() {
|
|
309
427
|
return this.name;
|
|
310
428
|
}
|
|
311
|
-
async getBranch(fromId) {
|
|
312
|
-
if (fromId === undefined)
|
|
313
|
-
return structuredClone(this.branch);
|
|
314
|
-
const byId = new Map(this.entries.map((entry) => [entry.id, entry]));
|
|
315
|
-
const result = [];
|
|
316
|
-
let current = byId.get(fromId);
|
|
317
|
-
while (current) {
|
|
318
|
-
result.push(current);
|
|
319
|
-
current = current.parentId === null ? undefined : byId.get(current.parentId);
|
|
320
|
-
}
|
|
321
|
-
return result.toReversed();
|
|
322
|
-
}
|
|
323
429
|
async buildSessionContext() {
|
|
324
|
-
return structuredClone(this.
|
|
430
|
+
return structuredClone(this.sessionContext);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async function withOfficeStorage(office, read) {
|
|
434
|
+
const storage = await OfficeStorage.acquire(office.sessionsPath);
|
|
435
|
+
try {
|
|
436
|
+
return await read(storage);
|
|
437
|
+
}
|
|
438
|
+
finally {
|
|
439
|
+
await storage.release();
|
|
325
440
|
}
|
|
326
441
|
}
|
|
327
442
|
export class SessionStore {
|
|
328
|
-
constructor(
|
|
329
|
-
this.
|
|
330
|
-
this.
|
|
331
|
-
this.
|
|
332
|
-
this.writerKey = writerKey;
|
|
443
|
+
constructor(storage, conversation, header) {
|
|
444
|
+
this.storage = storage;
|
|
445
|
+
this.conversation = conversation;
|
|
446
|
+
this.header = header;
|
|
333
447
|
this.mutationTail = Promise.resolve();
|
|
334
448
|
this.closed = false;
|
|
449
|
+
this.bound = false;
|
|
335
450
|
}
|
|
336
|
-
static async open(
|
|
337
|
-
const
|
|
338
|
-
const writerPath = writer.path;
|
|
451
|
+
static async open(office, key) {
|
|
452
|
+
const storage = await OfficeStorage.acquire(office.sessionsPath);
|
|
339
453
|
try {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const cwd = cwdOverride ?? process.cwd();
|
|
348
|
-
const built = buildHeader(cwd);
|
|
349
|
-
return new SessionStore(writerPath, cwd, { kind: "pending", ...built, fingerprint: fileFingerprint(writerPath) }, writer.key);
|
|
350
|
-
}
|
|
351
|
-
const header = parseCurrentHeader(writerPath, firstLine);
|
|
352
|
-
const opened = await openFileSession(writerPath, header);
|
|
353
|
-
const metadata = (await opened.session.getValue(MIKAN_METADATA, TODO_CONTEXT))?.value;
|
|
354
|
-
return new SessionStore(writerPath, cwdOverride ?? header.cwd, { kind: "live", header, metadata, ...opened }, writer.key);
|
|
454
|
+
const session = (await lookupSession(storage, key)) ?? (await createSession(storage, key));
|
|
455
|
+
storage.claimWriter(session.conversation.id, key);
|
|
456
|
+
return new SessionStore(storage, session.conversation, {
|
|
457
|
+
id: session.record.id,
|
|
458
|
+
createdAt: session.record.createdAt,
|
|
459
|
+
parentSessionId: await parentSessionId(storage, session.conversation.id),
|
|
460
|
+
});
|
|
355
461
|
}
|
|
356
462
|
catch (error) {
|
|
357
|
-
|
|
463
|
+
await storage.release();
|
|
358
464
|
throw error;
|
|
359
465
|
}
|
|
360
466
|
}
|
|
361
|
-
static async
|
|
362
|
-
return
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
467
|
+
static async forkRun(office, key, cause, entries) {
|
|
468
|
+
return withOfficeStorage(office, async (storage) => {
|
|
469
|
+
if ((await storage.index())[key])
|
|
470
|
+
return false;
|
|
471
|
+
const parent = await lookupSession(storage, cause.sessionKey);
|
|
472
|
+
if (!parent)
|
|
473
|
+
return false;
|
|
474
|
+
const at = runEnd(await readRecords(parent.conversation), cause.messageId);
|
|
475
|
+
if (at === undefined)
|
|
476
|
+
return false;
|
|
477
|
+
await parent.conversation.fork(at, {
|
|
478
|
+
ownership: { kind: "ownerless" },
|
|
479
|
+
init: async (tx, conversationId) => {
|
|
480
|
+
for (const entry of entries)
|
|
481
|
+
await appendImportedEntry(tx, conversationId, entry);
|
|
482
|
+
await writeSessionIndex(tx, key, {
|
|
483
|
+
conversationId,
|
|
484
|
+
id: randomUUID(),
|
|
485
|
+
createdAt: Date.now(),
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
}, context);
|
|
489
|
+
return true;
|
|
372
490
|
});
|
|
373
491
|
}
|
|
374
|
-
static async
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const name = await session.getName(TODO_CONTEXT);
|
|
379
|
-
const branchObject = await mainBranch(session, false);
|
|
380
|
-
const branch = branchObject
|
|
381
|
-
? await branchObject.findEntries({ order: "oldestFirst" }, TODO_CONTEXT)
|
|
382
|
-
: [];
|
|
383
|
-
const context = buildContext(branch);
|
|
384
|
-
return new CachedSessionInspection(headerView(header, metadata), entries, name, branch, context);
|
|
385
|
-
});
|
|
492
|
+
static async inMemory() {
|
|
493
|
+
const storage = await OfficeStorage.inMemory();
|
|
494
|
+
const conversation = await storage.harness.root(context);
|
|
495
|
+
return new SessionStore(storage, conversation, { id: randomUUID(), createdAt: Date.now() });
|
|
386
496
|
}
|
|
387
|
-
static async
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
let opened;
|
|
392
|
-
try {
|
|
393
|
-
const built = buildHeader(cwd, options);
|
|
394
|
-
mkdirSync(dirname(writerPath), { recursive: true });
|
|
395
|
-
writeFileSync(writerPath, `${JSON.stringify(built.header)}\n`, { mode: 0o600, flag: "wx" });
|
|
396
|
-
leaseKey = promotePendingWriter(leaseKey, writerPath);
|
|
397
|
-
opened = await openFileSession(writerPath, built.header);
|
|
398
|
-
if (built.metadata) {
|
|
399
|
-
await opened.session.setValue(MIKAN_METADATA, built.metadata, TODO_CONTEXT);
|
|
400
|
-
}
|
|
401
|
-
return new SessionStore(writerPath, cwd, { kind: "live", ...built, ...opened }, leaseKey);
|
|
402
|
-
}
|
|
403
|
-
catch (error) {
|
|
404
|
-
if (opened) {
|
|
405
|
-
await opened.session.close(TODO_CONTEXT).catch(() => undefined);
|
|
406
|
-
await opened.repo.close(TODO_CONTEXT).catch(() => undefined);
|
|
407
|
-
}
|
|
408
|
-
releaseWriter(leaseKey);
|
|
409
|
-
throw error;
|
|
410
|
-
}
|
|
497
|
+
static async exists(office, key) {
|
|
498
|
+
if (!existsSync(office.sessionsPath))
|
|
499
|
+
return false;
|
|
500
|
+
return withOfficeStorage(office, async (storage) => (await storage.index())[key] !== undefined);
|
|
411
501
|
}
|
|
412
|
-
static
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
return
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
432
|
-
const built = buildHeader(cwd, options);
|
|
433
|
-
atomicWritePrivateFile(path, `${JSON.stringify(built.header)}\n`);
|
|
434
|
-
}
|
|
435
|
-
static inMemory(cwd = process.cwd()) {
|
|
436
|
-
const built = buildHeader(cwd);
|
|
437
|
-
return new SessionStore(null, cwd, { kind: "pending", ...built, fingerprint: null }, null);
|
|
438
|
-
}
|
|
439
|
-
getSessionFile() {
|
|
440
|
-
this.assertOpen();
|
|
441
|
-
return this.sessionFile ?? undefined;
|
|
502
|
+
static async list(office) {
|
|
503
|
+
if (!existsSync(office.sessionsPath))
|
|
504
|
+
return [];
|
|
505
|
+
return withOfficeStorage(office, async (storage) => {
|
|
506
|
+
const index = await storage.index();
|
|
507
|
+
const byConversation = new Map(Object.values(index).map((record) => [record.conversationId, record.id]));
|
|
508
|
+
return Promise.all(Object.entries(index).map(async ([key, record]) => {
|
|
509
|
+
const parent = await storage.harness.commit(async (tx) => (await tx.conversation(record.conversationId))?.parent, context);
|
|
510
|
+
const parentId = parent ? byConversation.get(parent.conversationId) : undefined;
|
|
511
|
+
return {
|
|
512
|
+
key,
|
|
513
|
+
id: record.id,
|
|
514
|
+
createdAt: record.createdAt,
|
|
515
|
+
root: record.conversationId === ROOT_CONVERSATION_ID,
|
|
516
|
+
parentSessionId: parentId,
|
|
517
|
+
forkEntryId: parent && parentId ? `${parentId}:${parent.at}` : undefined,
|
|
518
|
+
};
|
|
519
|
+
}));
|
|
520
|
+
});
|
|
442
521
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
522
|
+
static async inspect(office, key) {
|
|
523
|
+
if (!existsSync(office.sessionsPath))
|
|
524
|
+
return undefined;
|
|
525
|
+
return withOfficeStorage(office, async (storage) => {
|
|
526
|
+
const session = await lookupSession(storage, key);
|
|
527
|
+
if (!session)
|
|
528
|
+
return undefined;
|
|
529
|
+
const { conversation, record } = session;
|
|
530
|
+
const name = (await storage.harness.snapshot(SessionDoc, conversation.id, context))?.name;
|
|
531
|
+
return new CachedSessionInspection({
|
|
532
|
+
id: record.id,
|
|
533
|
+
createdAt: record.createdAt,
|
|
534
|
+
parentSessionId: await parentSessionId(storage, conversation.id),
|
|
535
|
+
}, toSessionEntries(await readRecords(conversation), record.id), name, await readContext(conversation));
|
|
536
|
+
});
|
|
446
537
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
538
|
+
static async inspectExecution(office, key) {
|
|
539
|
+
if (!existsSync(office.sessionsPath))
|
|
540
|
+
return { open: false, started: false };
|
|
541
|
+
return withOfficeStorage(office, async (storage) => {
|
|
542
|
+
const session = await lookupSession(storage, key);
|
|
543
|
+
if (!session)
|
|
544
|
+
return { open: false, started: false };
|
|
545
|
+
const id = session.conversation.id;
|
|
546
|
+
const live = await storage.harness.snapshot(LiveDoc, id, context);
|
|
547
|
+
const run = (await storage.harness.snapshot(SessionDoc, id, context))?.run;
|
|
548
|
+
const open = live?.run !== undefined;
|
|
549
|
+
return {
|
|
550
|
+
open,
|
|
551
|
+
started: open || run !== undefined,
|
|
552
|
+
result: run?.endedAt !== undefined && run.status !== undefined
|
|
553
|
+
? { status: run.status, endedAt: run.endedAt }
|
|
554
|
+
: undefined,
|
|
555
|
+
};
|
|
556
|
+
});
|
|
450
557
|
}
|
|
451
558
|
getHeader() {
|
|
452
559
|
this.assertOpen();
|
|
453
|
-
return
|
|
560
|
+
return structuredClone(this.header);
|
|
454
561
|
}
|
|
455
562
|
getSessionId() {
|
|
456
563
|
this.assertOpen();
|
|
457
|
-
return this.
|
|
564
|
+
return this.header.id;
|
|
458
565
|
}
|
|
459
|
-
async
|
|
566
|
+
async getEntries() {
|
|
460
567
|
this.assertOpen();
|
|
461
|
-
|
|
462
|
-
return null;
|
|
463
|
-
return (await mainBranch(this.state.session, false))?.getTipId(TODO_CONTEXT) ?? null;
|
|
568
|
+
return toSessionEntries(await readRecords(this.conversation), this.header.id);
|
|
464
569
|
}
|
|
465
|
-
async
|
|
570
|
+
async getContextEntries() {
|
|
466
571
|
this.assertOpen();
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
return this.state.session.getEntry(id, TODO_CONTEXT);
|
|
572
|
+
const view = await this.conversation.context(context);
|
|
573
|
+
return toSessionEntries(view.entries, this.header.id);
|
|
470
574
|
}
|
|
471
|
-
async
|
|
575
|
+
async getContextTokens() {
|
|
472
576
|
this.assertOpen();
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
577
|
+
const view = await this.conversation.context(context);
|
|
578
|
+
const after = view.head?.id ?? Number.NEGATIVE_INFINITY;
|
|
579
|
+
for (const record of view.entries.toReversed()) {
|
|
580
|
+
const message = record.model?.[0];
|
|
581
|
+
if (record.id <= after || !AssistantEntry.is(record) || message?.role !== "assistant")
|
|
582
|
+
continue;
|
|
583
|
+
if (message.stopReason === "aborted" || message.stopReason === "error")
|
|
584
|
+
continue;
|
|
585
|
+
return calculateContextTokens(message.usage);
|
|
586
|
+
}
|
|
587
|
+
return undefined;
|
|
476
588
|
}
|
|
477
589
|
async getSessionName() {
|
|
478
590
|
this.assertOpen();
|
|
479
|
-
|
|
480
|
-
return undefined;
|
|
481
|
-
return this.state.session.getName(TODO_CONTEXT);
|
|
482
|
-
}
|
|
483
|
-
async getBranch(fromId) {
|
|
484
|
-
this.assertOpen();
|
|
485
|
-
if (this.state.kind === "pending")
|
|
486
|
-
return [];
|
|
487
|
-
if (fromId !== undefined) {
|
|
488
|
-
return this.state.session.scanBranch({ start: fromId, order: "oldestFirst" }, TODO_CONTEXT);
|
|
489
|
-
}
|
|
490
|
-
const branchObject = await mainBranch(this.state.session, false);
|
|
491
|
-
return branchObject ? branchObject.findEntries({ order: "oldestFirst" }, TODO_CONTEXT) : [];
|
|
591
|
+
return (await this.storage.harness.snapshot(SessionDoc, this.conversation.id, context))?.name;
|
|
492
592
|
}
|
|
493
593
|
async buildSessionContext() {
|
|
494
|
-
|
|
594
|
+
this.assertOpen();
|
|
595
|
+
return readContext(this.conversation);
|
|
495
596
|
}
|
|
496
597
|
async connectMcp(servers, signal) {
|
|
497
598
|
return this.mutate(async () => {
|
|
@@ -509,73 +610,104 @@ export class SessionStore {
|
|
|
509
610
|
const instructions = formatMcpServerInstructions(this.mcp?.instructions ?? []);
|
|
510
611
|
return instructions ? `${prompt}\n\n${instructions}` : prompt;
|
|
511
612
|
}
|
|
512
|
-
async
|
|
613
|
+
async bindHarness(binding) {
|
|
513
614
|
return this.mutate(async () => {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
615
|
+
this.storage.bind(this.conversation.id, binding);
|
|
616
|
+
this.bound = true;
|
|
617
|
+
const conversationId = this.conversation.id;
|
|
618
|
+
return {
|
|
619
|
+
harness: this.storage.harness,
|
|
620
|
+
conversation: this.conversation,
|
|
621
|
+
registry: this.storage.registry,
|
|
622
|
+
extensionName: `mikan.${conversationId}`,
|
|
623
|
+
tagRequest: (messages) => this.storage.tag(conversationId, messages),
|
|
624
|
+
};
|
|
521
625
|
});
|
|
522
626
|
}
|
|
523
627
|
async appendMessage(message) {
|
|
524
628
|
return this.mutate(async () => {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
629
|
+
const conversation = this.conversation;
|
|
630
|
+
const model = [durableMessage(message)];
|
|
631
|
+
const entry = await conversation.commit((tx) => {
|
|
632
|
+
switch (message.role) {
|
|
633
|
+
case "user":
|
|
634
|
+
return tx.appendEntry(UserEntry, conversation.id, { model });
|
|
635
|
+
case "assistant":
|
|
636
|
+
return tx.appendEntry(AssistantEntry, conversation.id, { model });
|
|
637
|
+
case "toolResult":
|
|
638
|
+
return tx.appendEntry(ToolResultEntry, conversation.id, {
|
|
639
|
+
model,
|
|
640
|
+
data: { diagnostics: [] },
|
|
641
|
+
});
|
|
642
|
+
default:
|
|
643
|
+
throw new Error("Only user, assistant, and tool result messages enter a session");
|
|
644
|
+
}
|
|
645
|
+
}, context);
|
|
646
|
+
return `${this.header.id}:${entry.id}`;
|
|
531
647
|
});
|
|
532
648
|
}
|
|
533
|
-
async appendCustomEntry(customType, data) {
|
|
649
|
+
async appendCustomEntry(customType, data, timestamp = Date.now()) {
|
|
534
650
|
return this.mutate(async () => {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
651
|
+
const conversation = this.conversation;
|
|
652
|
+
const entry = await conversation.commit((tx) => tx.appendEntry(CustomSessionEntry, conversation.id, {
|
|
653
|
+
data: { customType, data: toJson(data), timestamp },
|
|
654
|
+
}), context);
|
|
655
|
+
return `${this.header.id}:${entry.id}`;
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
async appendCompactionSummary(summary, timestamp) {
|
|
659
|
+
const text = wrapCompactionSummary(summary);
|
|
660
|
+
return this.mutate(async () => {
|
|
661
|
+
const conversation = this.conversation;
|
|
662
|
+
const entry = await conversation.commit((tx) => tx.appendEntry(CompactionEntry, conversation.id, {
|
|
663
|
+
model: [{ role: "user", content: [{ type: "text", text }], timestamp }],
|
|
664
|
+
data: { reason: "manual" },
|
|
665
|
+
head: "self",
|
|
666
|
+
}), context);
|
|
667
|
+
return `${this.header.id}:${entry.id}`;
|
|
542
668
|
});
|
|
543
669
|
}
|
|
544
|
-
async
|
|
545
|
-
|
|
670
|
+
async reset() {
|
|
671
|
+
await this.mutate(() => this.conversation.reset(undefined, context));
|
|
546
672
|
}
|
|
547
673
|
async setSessionName(name) {
|
|
548
|
-
await this.
|
|
674
|
+
await this.updateSessionDoc((doc) => {
|
|
675
|
+
doc.name = name.trim() || undefined;
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
async recordRun(run) {
|
|
679
|
+
await this.updateSessionDoc((doc) => {
|
|
680
|
+
doc.run = { ...run };
|
|
681
|
+
});
|
|
549
682
|
}
|
|
550
683
|
async close() {
|
|
551
684
|
if (this.closePromise)
|
|
552
685
|
return this.closePromise;
|
|
553
686
|
this.closed = true;
|
|
554
|
-
|
|
555
|
-
|
|
687
|
+
const conversationId = this.conversation.id;
|
|
688
|
+
this.closePromise = this.mutationTail.then(async () => {
|
|
556
689
|
try {
|
|
557
690
|
await this.mcp?.dispose();
|
|
558
691
|
}
|
|
559
692
|
finally {
|
|
560
|
-
if (this.
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
await this.harness.close(TODO_CONTEXT);
|
|
564
|
-
else
|
|
565
|
-
await this.state.session.close(TODO_CONTEXT);
|
|
566
|
-
}
|
|
567
|
-
finally {
|
|
568
|
-
await this.state.repo?.close(TODO_CONTEXT);
|
|
569
|
-
}
|
|
693
|
+
if (this.bound) {
|
|
694
|
+
this.storage.registry.uninstall({ name: `mikan.${conversationId}` });
|
|
695
|
+
this.storage.unbind(conversationId);
|
|
570
696
|
}
|
|
697
|
+
this.storage.releaseWriter(conversationId);
|
|
698
|
+
await this.storage.release();
|
|
571
699
|
}
|
|
572
|
-
})
|
|
573
|
-
.finally(() => {
|
|
574
|
-
if (this.writerKey !== null)
|
|
575
|
-
releaseWriter(this.writerKey);
|
|
576
700
|
});
|
|
577
701
|
return this.closePromise;
|
|
578
702
|
}
|
|
703
|
+
async updateSessionDoc(change) {
|
|
704
|
+
await this.mutate(async () => {
|
|
705
|
+
const conversation = this.conversation;
|
|
706
|
+
await conversation.commit(async (tx) => {
|
|
707
|
+
change(await tx.doc(SessionDoc, conversation.id));
|
|
708
|
+
}, context);
|
|
709
|
+
});
|
|
710
|
+
}
|
|
579
711
|
assertOpen() {
|
|
580
712
|
if (this.closed)
|
|
581
713
|
throw new Error("SessionStore is closed");
|
|
@@ -586,43 +718,5 @@ export class SessionStore {
|
|
|
586
718
|
this.mutationTail = result.then(() => undefined, () => undefined);
|
|
587
719
|
return result;
|
|
588
720
|
}
|
|
589
|
-
async live() {
|
|
590
|
-
if (this.state.kind === "live")
|
|
591
|
-
return this.state;
|
|
592
|
-
const pending = this.state;
|
|
593
|
-
const sessionFile = this.sessionFile;
|
|
594
|
-
if (sessionFile === null) {
|
|
595
|
-
const repo = new MemorySessionRepo();
|
|
596
|
-
const session = await repo.create({
|
|
597
|
-
id: pending.header.id,
|
|
598
|
-
parentSessionId: pending.header.parentSessionId,
|
|
599
|
-
}, TODO_CONTEXT);
|
|
600
|
-
const live = {
|
|
601
|
-
kind: "live",
|
|
602
|
-
header: pending.header,
|
|
603
|
-
metadata: pending.metadata,
|
|
604
|
-
session,
|
|
605
|
-
repo,
|
|
606
|
-
};
|
|
607
|
-
this.state = live;
|
|
608
|
-
return live;
|
|
609
|
-
}
|
|
610
|
-
materializePendingFile(sessionFile, pending.header, pending.fingerprint);
|
|
611
|
-
if (this.writerKey === null)
|
|
612
|
-
throw new Error("Pending session must have a writer lease");
|
|
613
|
-
this.writerKey = promotePendingWriter(this.writerKey, sessionFile);
|
|
614
|
-
const opened = await openFileSession(sessionFile, pending.header);
|
|
615
|
-
if (pending.metadata) {
|
|
616
|
-
await opened.session.setValue(MIKAN_METADATA, pending.metadata, TODO_CONTEXT);
|
|
617
|
-
}
|
|
618
|
-
const live = {
|
|
619
|
-
kind: "live",
|
|
620
|
-
header: pending.header,
|
|
621
|
-
metadata: pending.metadata,
|
|
622
|
-
...opened,
|
|
623
|
-
};
|
|
624
|
-
this.state = live;
|
|
625
|
-
return live;
|
|
626
|
-
}
|
|
627
721
|
}
|
|
628
722
|
//# sourceMappingURL=session-store.js.map
|