feat: improve session sharing and Kimi transcript support
- normalize Kimi thinking parts and omit empty placeholders - add standalone session-sharing and UI demo artifacts - add regression coverage and refresh the Kimi index marker - bump the Electron app and CLI packages to v0.2.1 - ignore generated app build assets
This commit is contained in:
+3
-1
@@ -9,4 +9,6 @@ release/
|
|||||||
dist/
|
dist/
|
||||||
app/out/
|
app/out/
|
||||||
HANDOFF.md
|
HANDOFF.md
|
||||||
.obelisk/
|
.obelisk/
|
||||||
|
app/build/
|
||||||
|
app/scripts/
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "obelisk",
|
"name": "obelisk",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obelisk",
|
"name": "obelisk",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^11.0.0",
|
"better-sqlite3": "^11.0.0",
|
||||||
"chokidar": "^4.0.3"
|
"chokidar": "^4.0.3"
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obelisk",
|
"name": "obelisk",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"description": "Memory management for Obelisk — let Claude Code search its own memory",
|
"description": "Memory management for Obelisk — let Claude Code search its own memory",
|
||||||
"homepage": "https://github.com/tommy0103/obelisk",
|
"homepage": "https://github.com/tommy0103/obelisk",
|
||||||
"author": {
|
"author": {
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"dist:mac": "electron-vite build && electron-builder --mac",
|
"dist:mac": "electron-vite build && electron-builder --mac",
|
||||||
"dist:win": "electron-vite build && electron-builder --win",
|
"dist:win": "electron-vite build && electron-builder --win",
|
||||||
"dist:linux": "electron-vite build && electron-builder --linux",
|
"dist:linux": "electron-vite build && electron-builder --linux",
|
||||||
|
"generate:session-share": "node scripts/generate-session-share.mjs",
|
||||||
"verify:icons": "node scripts/verify-icons.mjs",
|
"verify:icons": "node scripts/verify-icons.mjs",
|
||||||
"test:electron": "electron-vite build && electron --no-sandbox tests/electron-concurrency.mjs",
|
"test:electron": "electron-vite build && electron --no-sandbox tests/electron-concurrency.mjs",
|
||||||
"test:electron:timeline": "electron-vite build && electron --no-sandbox tests/electron-session-virtualization.mjs",
|
"test:electron:timeline": "electron-vite build && electron --no-sandbox tests/electron-session-virtualization.mjs",
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ function renderOutput(output, isError) {
|
|||||||
return `<div class="result-chip ${isError ? 'error' : ''}">${escapeHtml(output)}</div>`;
|
return `<div class="result-chip ${isError ? 'error' : ''}">${escapeHtml(output)}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPrettyTool(toolCall) {
|
export function renderPrettyTool(toolCall) {
|
||||||
const args = parseToolInput(toolCall);
|
const args = parseToolInput(toolCall);
|
||||||
const result = toolCall.result || {};
|
const result = toolCall.result || {};
|
||||||
const isError = Boolean(result.is_error);
|
const isError = Boolean(result.is_error);
|
||||||
|
|||||||
Generated
+1
-1
@@ -1289,7 +1289,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@obelisk-apps/cli",
|
"name": "@obelisk-apps/cli",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"obelisk": "dist/cli/src/obelisk.js"
|
"obelisk": "dist/cli/src/obelisk.js"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@obelisk-apps/cli",
|
"name": "@obelisk-apps/cli",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"description": "Local Obelisk runtime for coding agents.",
|
"description": "Local Obelisk runtime for coding agents.",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ interface ProjectedSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SOURCE = 'kimi';
|
const SOURCE = 'kimi';
|
||||||
export const KIMI_CANONICAL_TRANSCRIPT_MARKER = '__kimi_canonical_transcript_v2__';
|
export const KIMI_CANONICAL_TRANSCRIPT_MARKER = '__kimi_canonical_transcript_v3__';
|
||||||
|
|
||||||
function defaultKimiRoot(): string {
|
function defaultKimiRoot(): string {
|
||||||
return process.env['KIMI_CODE_HOME'] ?? join(homedir(), '.kimi-code');
|
return process.env['KIMI_CODE_HOME'] ?? join(homedir(), '.kimi-code');
|
||||||
@@ -141,12 +141,26 @@ function contentParts(content: unknown): JsonRecord[] {
|
|||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function partText(part: JsonRecord): string | null {
|
function rawPartText(part: JsonRecord): string | null {
|
||||||
if (part.type === 'text' && typeof part.text === 'string') return trunc(part.text);
|
if (part.type === 'text' && typeof part.text === 'string') return part.text;
|
||||||
if (part.type === 'thinking' && typeof part.thinking === 'string') return trunc(part.thinking);
|
if (part.type === 'think' && typeof part.think === 'string') return part.think;
|
||||||
|
if (part.type === 'thinking' && typeof part.thinking === 'string') return part.thinking;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function partText(part: JsonRecord): string | null {
|
||||||
|
const text = rawPartText(part);
|
||||||
|
return text === null ? null : trunc(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function partContentType(part: JsonRecord): string {
|
||||||
|
return part.type === 'think' || part.type === 'thinking'
|
||||||
|
? 'thinking'
|
||||||
|
: typeof part.type === 'string'
|
||||||
|
? part.type
|
||||||
|
: 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
function messageText(content: unknown): string | null {
|
function messageText(content: unknown): string | null {
|
||||||
const parts = contentParts(content);
|
const parts = contentParts(content);
|
||||||
const text = parts.map(partText).filter((value): value is string => value !== null);
|
const text = parts.map(partText).filter((value): value is string => value !== null);
|
||||||
@@ -154,7 +168,7 @@ function messageText(content: unknown): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function messageContentType(content: unknown): string {
|
function messageContentType(content: unknown): string {
|
||||||
const types = new Set(contentParts(content).map((part) => String(part.type ?? 'unknown')));
|
const types = new Set(contentParts(content).map(partContentType));
|
||||||
return types.size === 1 ? [...types][0]! : 'unknown';
|
return types.size === 1 ? [...types][0]! : 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,6 +451,8 @@ function projectSession(meta: KimiSessionUnitMeta, sessionId: string, state: Jso
|
|||||||
if (event.type === 'content.part' && typeof event.stepUuid === 'string') {
|
if (event.type === 'content.part' && typeof event.stepUuid === 'string') {
|
||||||
const part = event.part as JsonRecord | undefined;
|
const part = event.part as JsonRecord | undefined;
|
||||||
if (part === undefined) continue;
|
if (part === undefined) continue;
|
||||||
|
const text = partText(part);
|
||||||
|
if (text === null || text.trim().length === 0) continue;
|
||||||
pushMessage({
|
pushMessage({
|
||||||
kind: 'message',
|
kind: 'message',
|
||||||
uuid: namespacedEventId(sessionId, wire.agentId, event.uuid, line),
|
uuid: namespacedEventId(sessionId, wire.agentId, event.uuid, line),
|
||||||
@@ -445,8 +461,8 @@ function projectSession(meta: KimiSessionUnitMeta, sessionId: string, state: Jso
|
|||||||
parent_uuid: previousUuid,
|
parent_uuid: previousUuid,
|
||||||
timestamp,
|
timestamp,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
text: partText(part),
|
text,
|
||||||
content_type: typeof part.type === 'string' ? part.type : 'unknown',
|
content_type: partContentType(part),
|
||||||
is_meta: 0,
|
is_meta: 0,
|
||||||
visibility: 'visible',
|
visibility: 'visible',
|
||||||
model,
|
model,
|
||||||
@@ -604,17 +620,14 @@ function rawFromWire(path: string, messageUuid: string): RawRecord | null {
|
|||||||
if (slashCommand !== null) projectedText = slashCommand;
|
if (slashCommand !== null) projectedText = slashCommand;
|
||||||
else {
|
else {
|
||||||
const parts = contentParts(message.content).map((part) => {
|
const parts = contentParts(message.content).map((part) => {
|
||||||
if (part.type === 'text' && typeof part.text === 'string') return part.text;
|
return rawPartText(part);
|
||||||
if (part.type === 'thinking' && typeof part.thinking === 'string') return part.thinking;
|
|
||||||
return null;
|
|
||||||
}).filter((part): part is string => part !== null);
|
}).filter((part): part is string => part !== null);
|
||||||
projectedText = parts.length > 0 ? parts.join('\n') : null;
|
projectedText = parts.length > 0 ? parts.join('\n') : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (record.type === 'context.append_loop_event') {
|
} else if (record.type === 'context.append_loop_event') {
|
||||||
const part = (record.event as JsonRecord | undefined)?.part as JsonRecord | undefined;
|
const part = (record.event as JsonRecord | undefined)?.part as JsonRecord | undefined;
|
||||||
if (part?.type === 'text' && typeof part.text === 'string') projectedText = part.text;
|
if (part !== undefined) projectedText = rawPartText(part);
|
||||||
if (part?.type === 'thinking' && typeof part.thinking === 'string') projectedText = part.thinking;
|
|
||||||
}
|
}
|
||||||
} catch { /* malformed torn source line */ }
|
} catch { /* malformed torn source line */ }
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ test('Kimi undo and clear replace the indexed session instead of leaving stale r
|
|||||||
db.close();
|
db.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Kimi prompt semantics marker replays unchanged sessions once', () => {
|
test('Kimi canonical transcript marker replays unchanged sessions once', () => {
|
||||||
const home = mkdtempSync(join(tmpdir(), 'obelisk-kimi-prompt-marker-'));
|
const home = mkdtempSync(join(tmpdir(), 'obelisk-kimi-prompt-marker-'));
|
||||||
const claudeDir = join(home, '.claude');
|
const claudeDir = join(home, '.claude');
|
||||||
const codexDir = join(home, '.codex');
|
const codexDir = join(home, '.codex');
|
||||||
@@ -151,7 +151,7 @@ test('Kimi prompt semantics marker replays unchanged sessions once', () => {
|
|||||||
buildIndex(options);
|
buildIndex(options);
|
||||||
let db = new TestDatabase(dbPath);
|
let db = new TestDatabase(dbPath);
|
||||||
const marker = createKimiProvider({ rootDir: kimiDir }).indexVersionMarker;
|
const marker = createKimiProvider({ rootDir: kimiDir }).indexVersionMarker;
|
||||||
assert.equal(typeof marker, 'string');
|
assert.equal(marker, '__kimi_canonical_transcript_v3__');
|
||||||
db.prepare("UPDATE messages SET text='stale expanded instructions', is_meta=1 WHERE source='kimi'").run();
|
db.prepare("UPDATE messages SET text='stale expanded instructions', is_meta=1 WHERE source='kimi'").run();
|
||||||
db.prepare('DELETE FROM index_state WHERE jsonl_path=?').run(marker);
|
db.prepare('DELETE FROM index_state WHERE jsonl_path=?').run(marker);
|
||||||
db.close();
|
db.close();
|
||||||
|
|||||||
@@ -145,6 +145,52 @@ test('kimi provider ignores a torn final wire line until it is completed', () =>
|
|||||||
assert.equal(ret, unit.meta.currentCursor);
|
assert.equal(ret, unit.meta.currentCursor);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('kimi provider normalizes think parts and drops empty thinking placeholders', () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'obelisk-kimi-think-'));
|
||||||
|
const sessionDir = join(root, 'sessions', 'workspace-1', 'session-think-1');
|
||||||
|
const mainDir = join(sessionDir, 'agents', 'main');
|
||||||
|
const wirePath = join(mainDir, 'wire.jsonl');
|
||||||
|
mkdirSync(mainDir, { recursive: true });
|
||||||
|
writeFileSync(join(sessionDir, 'state.json'), JSON.stringify({ workDir: '/tmp/think' }));
|
||||||
|
const records = [
|
||||||
|
{ type: 'metadata', protocol_version: '1.5', created_at: 1 },
|
||||||
|
{ type: 'context.append_loop_event', time: 2, event: { type: 'step.begin', uuid: 'step-1' } },
|
||||||
|
{ type: 'context.append_loop_event', time: 3, event: {
|
||||||
|
type: 'content.part', uuid: 'think-1', stepUuid: 'step-1',
|
||||||
|
part: { type: 'think', think: 'private reasoning' },
|
||||||
|
} },
|
||||||
|
{ type: 'context.append_loop_event', time: 4, event: {
|
||||||
|
type: 'content.part', uuid: 'think-empty', stepUuid: 'step-1',
|
||||||
|
part: { type: 'think', think: '' },
|
||||||
|
} },
|
||||||
|
{ type: 'context.append_loop_event', time: 5, event: {
|
||||||
|
type: 'content.part', uuid: 'answer-1', stepUuid: 'step-1',
|
||||||
|
part: { type: 'text', text: 'visible answer' },
|
||||||
|
} },
|
||||||
|
];
|
||||||
|
writeFileSync(wirePath, records.map(record => JSON.stringify(record)).join('\n') + '\n');
|
||||||
|
const provider = createKimiProvider({ rootDir: root });
|
||||||
|
const unit = provider.discover({ lastCursor: () => null })[0];
|
||||||
|
|
||||||
|
const { values } = drain(provider.parse(unit, null));
|
||||||
|
const messages = values.filter(record => record.kind === 'message');
|
||||||
|
|
||||||
|
assert.deepEqual(messages.map(record => ({
|
||||||
|
text: record.text,
|
||||||
|
content_type: record.content_type,
|
||||||
|
})), [
|
||||||
|
{ text: 'private reasoning', content_type: 'thinking' },
|
||||||
|
{ text: 'visible answer', content_type: 'text' },
|
||||||
|
]);
|
||||||
|
assert.equal(values.find(record => record.kind === 'session').message_count, 2);
|
||||||
|
assert.equal(provider.raw({
|
||||||
|
source: 'kimi',
|
||||||
|
messageUuid: messages[0].uuid,
|
||||||
|
session: { jsonl_path: wirePath },
|
||||||
|
agentId: null,
|
||||||
|
}).messageText, 'private reasoning');
|
||||||
|
});
|
||||||
|
|
||||||
test('kimi provider replays clear and undo markers with Kimi transcript semantics', () => {
|
test('kimi provider replays clear and undo markers with Kimi transcript semantics', () => {
|
||||||
const root = mkdtempSync(join(tmpdir(), 'obelisk-kimi-undo-'));
|
const root = mkdtempSync(join(tmpdir(), 'obelisk-kimi-undo-'));
|
||||||
const sessionDir = join(root, 'sessions', 'workspace-1', 'session-undo-1');
|
const sessionDir = join(root, 'sessions', 'workspace-1', 'session-undo-1');
|
||||||
|
|||||||
Reference in New Issue
Block a user