kimaki 1.3.0 → 1.4.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/dist/bot.d.ts +3 -5
- package/dist/bot.js +3 -9
- package/dist/bot.js.map +1 -1
- package/dist/builtin-skills.d.ts +3 -0
- package/dist/builtin-skills.js +29 -0
- package/dist/builtin-skills.js.map +1 -1
- package/dist/builtin-skills.test.js +73 -1
- package/dist/builtin-skills.test.js.map +1 -1
- package/dist/cli/bot.d.ts +1 -1
- package/dist/cli/bot.js +8 -4
- package/dist/cli/bot.js.map +1 -1
- package/dist/cli-lock.e2e.test.js +1 -1
- package/dist/event-loop.d.ts +1 -2
- package/dist/event-loop.js +2 -2
- package/dist/event-loop.js.map +1 -1
- package/dist/main.d.ts +2 -1
- package/dist/main.js +2 -2
- package/dist/main.js.map +1 -1
- package/dist/markdown/markdown.test.js +10 -0
- package/dist/markdown/markdown.test.js.map +1 -1
- package/dist/markdown/render-markdown.js +3 -1
- package/dist/markdown/render-markdown.js.map +1 -1
- package/dist/plugin/index.js +17 -44
- package/dist/plugin/index.js.map +1 -1
- package/dist/session-events.js +9 -5
- package/dist/session-events.js.map +1 -1
- package/dist/sessions.js +4 -3
- package/dist/sessions.js.map +1 -1
- package/dist/system-prompt.d.ts +3 -15
- package/dist/system-prompt.js +52 -77
- package/dist/system-prompt.js.map +1 -1
- package/dist/test/harness.d.ts +1 -2
- package/dist/test/harness.js +1 -2
- package/dist/test/harness.js.map +1 -1
- package/dist/test/replay.js +0 -1
- package/dist/test/replay.js.map +1 -1
- package/dist/thread-reducer.d.ts +0 -3
- package/dist/thread-reducer.fixtures.test.js +2 -2
- package/dist/thread-reducer.fixtures.test.js.map +1 -1
- package/dist/thread-reducer.js +4 -32
- package/dist/thread-reducer.js.map +1 -1
- package/dist/thread-reducer.test.js +0 -31
- package/dist/thread-reducer.test.js.map +1 -1
- package/dist/tunnel.d.ts +9 -0
- package/dist/tunnel.js +117 -28
- package/dist/tunnel.js.map +1 -1
- package/dist/tunnel.test.js +81 -5
- package/dist/tunnel.test.js.map +1 -1
- package/package.json +4 -4
- package/skills/kimaki-tunnel/SKILL.md +3 -2
- package/src/bot.ts +7 -14
- package/src/builtin-skills.test.ts +74 -1
- package/src/builtin-skills.ts +33 -0
- package/src/cli/bot.ts +7 -5
- package/src/cli-lock.e2e.test.ts +1 -1
- package/src/event-loop.ts +1 -4
- package/src/main.ts +5 -4
- package/src/markdown/markdown.test.ts +11 -0
- package/src/markdown/render-markdown.ts +3 -1
- package/src/plugin/index.ts +18 -44
- package/src/session-events.ts +8 -4
- package/src/sessions.ts +3 -3
- package/src/system-prompt.ts +56 -80
- package/src/test/harness.ts +0 -3
- package/src/test/replay.ts +0 -1
- package/src/thread-reducer.fixtures.test.ts +2 -2
- package/src/thread-reducer.test.ts +0 -31
- package/src/thread-reducer.ts +4 -36
- package/src/tunnel.test.ts +81 -5
- package/src/tunnel.ts +124 -25
- package/src/message-tool.e2e.test.ts +0 -45
package/src/cli/bot.ts
CHANGED
|
@@ -8,10 +8,10 @@ import fs from 'node:fs'
|
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
10
10
|
import dedent from 'string-dedent'
|
|
11
|
-
import type
|
|
11
|
+
import { wrapJsonSchema, type Goke } from 'goke'
|
|
12
12
|
|
|
13
13
|
import { distCliEntry, writeAutostartScript } from '../autostart.ts'
|
|
14
|
-
import { parseDisabledSkills } from '../builtin-skills.ts'
|
|
14
|
+
import { parseDisabledPromptSections, parseDisabledSkills, PROMPT_SECTIONS } from '../builtin-skills.ts'
|
|
15
15
|
import { callBot, DEFAULT_LOCK_PORT, RESTART_EXIT_CODE, startLockServer } from '../lock-server.ts'
|
|
16
16
|
import { parseDuration } from '../duration.ts'
|
|
17
17
|
import { createLogger, setLogFile } from '../logger.ts'
|
|
@@ -89,9 +89,9 @@ export function registerStartCommand(cli: Goke) {
|
|
|
89
89
|
.option('--restart-onboarding', 'Choose credentials again')
|
|
90
90
|
.option('--worktrees', 'Use a fresh Git worktree for new sessions unless the channel overrides it')
|
|
91
91
|
.option('--allow-all-users', 'Every server member can use the bot (role "no-kimaki" still denies; provider login stays admin-only)')
|
|
92
|
-
.option('--message-tool', 'Discord shows only a concise reply the agent sends with a send_message tool; plain text stays visible in the OpenCode TUI')
|
|
93
92
|
.option('--voice <name>', 'Voice of voice calls, saved for later starts (e.g. cove, juniper, sol). "default" resets it')
|
|
94
93
|
.option('--disable-skills <list>', 'Built-in Kimaki skills to turn off: "all" or a comma separated list (kimaki-debug,kimaki-tunnel)')
|
|
94
|
+
.option('--disable-prompt <section>', wrapJsonSchema<string[]>({ type: 'array', items: { type: 'string' }, description: `System prompt section to remove from new sessions (repeatable): ${PROMPT_SECTIONS.join(', ')}` }))
|
|
95
95
|
.option('--no-analytics', 'Disable anonymous usage analytics (same as KIMAKI_STRADA_ENABLED=0)')
|
|
96
96
|
.option('--if-not-running', 'Start only if no bot runs; never stop the running one (used by autostart)')
|
|
97
97
|
.action(async (options) => {
|
|
@@ -130,6 +130,8 @@ export function registerStartCommand(cli: Goke) {
|
|
|
130
130
|
const urls = gatewayUrlsFromEnv()
|
|
131
131
|
const disabledSkills = parseDisabledSkills(options.disableSkills)
|
|
132
132
|
if (disabledSkills instanceof Error) fail(disabledSkills)
|
|
133
|
+
const disabledPrompt = parseDisabledPromptSections(options.disablePrompt)
|
|
134
|
+
if (disabledPrompt instanceof Error) fail(disabledPrompt)
|
|
133
135
|
const machine = options.machineName ?? defaultMachineName()
|
|
134
136
|
// Validated before the lock, so a typo never stops the running bot. null: reset to the default.
|
|
135
137
|
const voice = (() => {
|
|
@@ -220,7 +222,7 @@ export function registerStartCommand(cli: Goke) {
|
|
|
220
222
|
autoWorktrees: Boolean(options.worktrees),
|
|
221
223
|
allowAllUsers: Boolean(options.allowAllUsers),
|
|
222
224
|
disabledSkills,
|
|
223
|
-
|
|
225
|
+
disabledPrompt,
|
|
224
226
|
})
|
|
225
227
|
if (bot instanceof Error) failStartup(bot)
|
|
226
228
|
// exit() keeps process.exitCode: the restart route sets RESTART_EXIT_CODE before its SIGTERM.
|
|
@@ -250,7 +252,7 @@ export function registerStartCommand(cli: Goke) {
|
|
|
250
252
|
...(options.worktrees ? ['--worktrees'] : []),
|
|
251
253
|
...(options.allowAllUsers ? ['--allow-all-users'] : []),
|
|
252
254
|
...(options.disableSkills ? ['--disable-skills', options.disableSkills] : []),
|
|
253
|
-
...(
|
|
255
|
+
...disabledPrompt.flatMap((section) => ['--disable-prompt', section]),
|
|
254
256
|
...(options.noAnalytics ? ['--no-analytics'] : []),
|
|
255
257
|
...(options.machineName ? ['--machine-name', options.machineName] : []),
|
|
256
258
|
] })
|
package/src/cli-lock.e2e.test.ts
CHANGED
|
@@ -376,9 +376,9 @@ test('CLI help documents the supported P7 commands', async () => {
|
|
|
376
376
|
--restart-onboarding Choose credentials again
|
|
377
377
|
--worktrees Use a fresh Git worktree for new sessions unless the channel overrides it
|
|
378
378
|
--allow-all-users Every server member can use the bot (role "no-kimaki" still denies; provider login stays admin-only)
|
|
379
|
-
--message-tool Discord shows only a concise reply the agent sends with a send_message tool; plain text stays visible in the OpenCode TUI
|
|
380
379
|
--voice <name> Voice of voice calls, saved for later starts (e.g. cove, juniper, sol). "default" resets it
|
|
381
380
|
--disable-skills <list> Built-in Kimaki skills to turn off: "all" or a comma separated list (kimaki-debug,kimaki-tunnel)
|
|
381
|
+
--disable-prompt <section> System prompt section to remove from new sessions (repeatable): discord-permissions, kimaki-debugging, file-upload-requests, action-buttons, session-sleep, session-abort, cross-project-commands, waiting-sessions, critique-diffs, markdown-formatting, discord-callouts, search-result-urls, ascii-diagrams
|
|
382
382
|
--no-analytics Disable anonymous usage analytics (same as KIMAKI_STRADA_ENABLED=0)
|
|
383
383
|
--if-not-running Start only if no bot runs; never stop the running one (used by autostart)
|
|
384
384
|
-h, --help Display this message
|
package/src/event-loop.ts
CHANGED
|
@@ -67,7 +67,6 @@ export function createEventLoop({
|
|
|
67
67
|
effects,
|
|
68
68
|
recorder,
|
|
69
69
|
analytics,
|
|
70
|
-
messageTool,
|
|
71
70
|
onRunEnded,
|
|
72
71
|
onTranscriptDue,
|
|
73
72
|
}: {
|
|
@@ -77,8 +76,6 @@ export function createEventLoop({
|
|
|
77
76
|
effects: EffectsRunner
|
|
78
77
|
recorder: EventRecorder
|
|
79
78
|
analytics: Analytics
|
|
80
|
-
// `kimaki --message-tool`: plain text of root sessions stays hidden (Prefs.messageTool).
|
|
81
|
-
messageTool: boolean
|
|
82
79
|
// Every finished or failed run of a thread's root session (voice call notifications).
|
|
83
80
|
onRunEnded: (run: RunEnded) => void
|
|
84
81
|
// A root session stopped running (also after an interrupt): its transcript file is stale.
|
|
@@ -125,7 +122,7 @@ export function createEventLoop({
|
|
|
125
122
|
if (!view) return
|
|
126
123
|
const verbosity = state.verbosity[view.channelId] ?? verbosityFromV1(null)
|
|
127
124
|
const contextLimits = modelLimits.get(view.directory)?.limits ?? {}
|
|
128
|
-
const result = reduce({ view, event, prefs: { verbosity, contextLimits
|
|
125
|
+
const result = reduce({ view, event, prefs: { verbosity, contextLimits } })
|
|
129
126
|
if (result.view !== view) store.setState((current) => ({ threads: { ...current.threads, [threadId]: result.view } }))
|
|
130
127
|
effects.run(threadId, result.effects)
|
|
131
128
|
if (event.type === 'session.moved' && event.data.sessionID === view.sessionId) void loadModelLimits(event.data.location.directory)
|
package/src/main.ts
CHANGED
|
@@ -9,6 +9,7 @@ import * as errore from 'errore'
|
|
|
9
9
|
import { createAgentUi } from './agent-ui.ts'
|
|
10
10
|
import type { Analytics } from './analytics.ts'
|
|
11
11
|
import { oc, type Bot } from './bot.ts'
|
|
12
|
+
import type { PromptSection } from './builtin-skills.ts'
|
|
12
13
|
import { openDb } from './db.ts'
|
|
13
14
|
import { createEffectsRunner } from './effects.ts'
|
|
14
15
|
import { DiscordError } from './errors.ts'
|
|
@@ -68,8 +69,8 @@ export type StartBotOptions = {
|
|
|
68
69
|
allowAllUsers?: boolean
|
|
69
70
|
// Built-in skill ids denied in new sessions (builtin-skills.ts).
|
|
70
71
|
disabledSkills?: readonly string[]
|
|
71
|
-
//
|
|
72
|
-
|
|
72
|
+
// Static instruction parts left out of new sessions (system-prompt.ts).
|
|
73
|
+
disabledPrompt?: readonly PromptSection[]
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
export type BotHandle = Bot & {
|
|
@@ -148,7 +149,7 @@ export async function startBot(options: StartBotOptions): Promise<Error | BotHan
|
|
|
148
149
|
if (result instanceof Error) logger.warn(`transcript of ${sessionId} not synced`, result)
|
|
149
150
|
})
|
|
150
151
|
}
|
|
151
|
-
const eventLoop = createEventLoop({ store, db, discord, effects, recorder, analytics: options.analytics,
|
|
152
|
+
const eventLoop = createEventLoop({ store, db, discord, effects, recorder, analytics: options.analytics, onRunEnded: voiceCalls.runEnded, onTranscriptDue })
|
|
152
153
|
const loaded = await eventLoop.load()
|
|
153
154
|
if (loaded instanceof Error) return loaded
|
|
154
155
|
// Before the service is used, so a service started by ensure() loads it at once.
|
|
@@ -187,7 +188,7 @@ export async function startBot(options: StartBotOptions): Promise<Error | BotHan
|
|
|
187
188
|
autoWorktrees: options.autoWorktrees ?? false,
|
|
188
189
|
allowAllUsers: options.allowAllUsers ?? false,
|
|
189
190
|
disabledSkills: options.disabledSkills ?? [],
|
|
190
|
-
|
|
191
|
+
disabledPrompt: options.disabledPrompt ?? [],
|
|
191
192
|
features: { withSleepLock: createSleepLock(), waitForPlugin: createPluginWait({ opencode }), agentUi, voiceCalls },
|
|
192
193
|
}
|
|
193
194
|
const scheduler = createScheduler({
|
|
@@ -412,3 +412,14 @@ test('edge cases: 2500-char code line, fence info line inside a fence, oversized
|
|
|
412
412
|
|
|
413
413
|
expect(markdownPayloads('<callout accent="#ef4444"></callout>')).toMatchInlineSnapshot(`[]`)
|
|
414
414
|
})
|
|
415
|
+
|
|
416
|
+
test('empty blockquote lines get a trailing space so Discord keeps the quote', () => {
|
|
417
|
+
const text = '**1. To a@b.com**\n\n> Hi Mohammad,\n>\n> Your subscription is cancelled.\n>\n> Tommy\n\nDone.'
|
|
418
|
+
const segments = renderMarkdown(text)
|
|
419
|
+
expect(segments.map((segment) => ('markdown' in segment ? JSON.stringify(segment.markdown.split('\n')) : segment.kind)))
|
|
420
|
+
.toMatchInlineSnapshot(`
|
|
421
|
+
[
|
|
422
|
+
"["**1. To a@b.com**","","> Hi Mohammad,","> ","> Your subscription is cancelled.","> ","> Tommy","","Done."]",
|
|
423
|
+
]
|
|
424
|
+
`)
|
|
425
|
+
})
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// ├─ table ──▶ table segment (Components V2 later)
|
|
6
6
|
// ├─ heading depth > 3 ──▶ clamped to ###
|
|
7
7
|
// ├─ list with code items ──▶ code lifted after its item
|
|
8
|
+
// ├─ blockquote ──▶ bare ">" lines become "> "
|
|
8
9
|
// └─ other nodes ──▶ original source slices, packed into ≤ limit
|
|
9
10
|
//
|
|
10
11
|
// Text is sliced from the original source by node offsets, never
|
|
@@ -217,7 +218,8 @@ function nodePieces(source: string, node: RootContent, limit: number): string[]
|
|
|
217
218
|
return unnestList(source, node, limit).flatMap((piece) => (piece.length <= limit ? [piece] : splitText(piece, limit)))
|
|
218
219
|
}
|
|
219
220
|
if (node.type === 'code') return codePieces(source, node, limit)
|
|
220
|
-
|
|
221
|
+
// Discord quotes need "> " plus a space; a bare ">" line shows as a literal ">".
|
|
222
|
+
const text = node.type === 'blockquote' ? slice(source, node).replace(/^( *>)$/gm, '$1 ') : slice(source, node)
|
|
221
223
|
if (text.length <= limit) return [text]
|
|
222
224
|
if (node.type === 'list') {
|
|
223
225
|
const items = node.children.map((item) => slice(source, item))
|
package/src/plugin/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { JsonValue } from '@opencode/client'
|
|
2
1
|
import { Plugin, Skill } from '@opencode/plugin'
|
|
2
|
+
import type { SessionContext } from '@opencode/plugin/promise/session'
|
|
3
3
|
import { Schema } from 'effect'
|
|
4
4
|
import { execFile } from 'node:child_process'
|
|
5
5
|
import { promisify } from 'node:util'
|
|
@@ -8,7 +8,7 @@ import path from 'node:path'
|
|
|
8
8
|
import { autostartBot } from '../autostart.ts'
|
|
9
9
|
import { readBuiltinSkills } from '../builtin-skills.ts'
|
|
10
10
|
import { fileEditTool, recordToolEdits } from '../file-edit-log.ts'
|
|
11
|
-
import {
|
|
11
|
+
import { turnContext } from '../system-prompt.ts'
|
|
12
12
|
|
|
13
13
|
const exec = promisify(execFile)
|
|
14
14
|
|
|
@@ -32,32 +32,17 @@ export default Plugin.define({
|
|
|
32
32
|
}
|
|
33
33
|
})
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
async function marker(sessionID: string): Promise<{ fields: Map<string, JsonValue>; root: boolean } | null> {
|
|
35
|
+
async function marker(sessionID: string) {
|
|
37
36
|
const session = await ctx.session.get({ sessionID }).catch(() => null)
|
|
38
37
|
const value: unknown = session?.metadata?.['kimaki']
|
|
39
38
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
|
40
39
|
const current = new Map(Object.entries(value))
|
|
41
|
-
if (!session?.parentID) return
|
|
40
|
+
if (!session?.parentID) return current
|
|
42
41
|
const parent = await ctx.session.get({ sessionID: session.parentID }).catch(() => null)
|
|
43
42
|
const parentMarker = parent?.metadata?.['kimaki']
|
|
44
|
-
if (parentMarker && typeof parentMarker === 'object' && !Array.isArray(parentMarker) && parentMarker['threadId'] === current.get('threadId'))
|
|
45
|
-
|
|
46
|
-
return root && { fields: root.fields, root: false }
|
|
47
|
-
}
|
|
48
|
-
return { fields: current, root: true }
|
|
43
|
+
if (parentMarker && typeof parentMarker === 'object' && !Array.isArray(parentMarker) && parentMarker['threadId'] === current.get('threadId')) return marker(session.parentID)
|
|
44
|
+
return current
|
|
49
45
|
}
|
|
50
|
-
// Registered for every session; the context hook removes it outside message mode.
|
|
51
|
-
await ctx.tool.transform((editor) => {
|
|
52
|
-
editor.add({
|
|
53
|
-
name: MESSAGE_TOOL,
|
|
54
|
-
description: MESSAGE_TOOL_DESCRIPTION,
|
|
55
|
-
input: MESSAGE_TOOL_INPUT,
|
|
56
|
-
options: { codemode: false },
|
|
57
|
-
// Discord renders the call input (thread-reducer.ts); the tool itself does nothing.
|
|
58
|
-
execute: async () => ({ content: 'Message sent to the user.' }),
|
|
59
|
-
})
|
|
60
|
-
})
|
|
61
46
|
// The git branch reaches the model as a synthetic message saved right before
|
|
62
47
|
// the user prompt, only when it changed. Text in event.system would change the
|
|
63
48
|
// cached prompt prefix on every branch switch. The prompt hook runs before
|
|
@@ -74,28 +59,14 @@ export default Plugin.define({
|
|
|
74
59
|
if (last?.type === 'synthetic' && last.text === text) return
|
|
75
60
|
await ctx.session.synthetic({ sessionID: event.sessionID, text, delivery: event.delivery, resume: false }).catch(() => undefined)
|
|
76
61
|
})
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
async
|
|
81
|
-
const info = await marker(event.sessionID)
|
|
82
|
-
const messageMode = info?.root === true && info.fields.get('messageTool') === true
|
|
83
|
-
if (!messageMode) delete event.tools[MESSAGE_TOOL]
|
|
84
|
-
else event.system.push({ type: 'text', text: MESSAGE_MODE_INSTRUCTIONS })
|
|
85
|
-
return info
|
|
86
|
-
}
|
|
87
|
-
await ctx.session.hook('compaction', async (event) => {
|
|
88
|
-
await applyMessageMode(event)
|
|
89
|
-
})
|
|
90
|
-
await ctx.session.hook('generate', async (event) => {
|
|
91
|
-
await applyMessageMode(event)
|
|
92
|
-
})
|
|
93
|
-
await ctx.session.hook('context', async (event) => {
|
|
62
|
+
// Runs for every request kind: turns (context), compaction and generate (OpenCode
|
|
63
|
+
// `warming` keep-alive requests). All kinds must get the same system text, tools
|
|
64
|
+
// and messages, or the provider prompt cache misses.
|
|
65
|
+
const shapeRequest = async (event: SessionContext) => {
|
|
94
66
|
// The only prompt change for every session, TUI included. The ID never
|
|
95
67
|
// changes in a session, so the cached prompt prefix stays stable.
|
|
96
68
|
event.system.push({ type: 'text', text: `OpenCode session ID: ${event.sessionID}` })
|
|
97
|
-
|
|
98
|
-
if (!info) return
|
|
69
|
+
if (!(await marker(event.sessionID))) return
|
|
99
70
|
// The <discord-user> context of each prompt, built from its metadata, so the
|
|
100
71
|
// stored text stays clean for the TUI and other clients. Same metadata, same
|
|
101
72
|
// text on every request: the cached prompt prefix stays stable.
|
|
@@ -121,12 +92,15 @@ export default Plugin.define({
|
|
|
121
92
|
...properties,
|
|
122
93
|
} }
|
|
123
94
|
}
|
|
124
|
-
}
|
|
95
|
+
}
|
|
96
|
+
await ctx.session.hook('context', shapeRequest)
|
|
97
|
+
await ctx.session.hook('compaction', shapeRequest)
|
|
98
|
+
await ctx.session.hook('generate', shapeRequest)
|
|
125
99
|
await ctx.tool.hook('execute.before', async (event) => {
|
|
126
100
|
if (event.tool !== 'shell' || !event.input || typeof event.input !== 'object' || Array.isArray(event.input)) return
|
|
127
101
|
const info = await marker(event.sessionID)
|
|
128
|
-
const dataDir = info?.
|
|
129
|
-
const lockPort = info?.
|
|
102
|
+
const dataDir = info?.get('dataDir')
|
|
103
|
+
const lockPort = info?.get('lockPort')
|
|
130
104
|
if (typeof dataDir !== 'string' || typeof lockPort !== 'number') return
|
|
131
105
|
if (!('command' in event.input) || typeof event.input.command !== 'string') return
|
|
132
106
|
const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`
|
|
@@ -135,7 +109,7 @@ export default Plugin.define({
|
|
|
135
109
|
// File edits feed `kimaki session editors` (v1-compatible JSONL index).
|
|
136
110
|
await ctx.tool.hook('execute.after', async (event) => {
|
|
137
111
|
if (event.status !== 'completed' || !fileEditTool(event.tool)) return
|
|
138
|
-
const dataDir = (await marker(event.sessionID))?.
|
|
112
|
+
const dataDir = (await marker(event.sessionID))?.get('dataDir')
|
|
139
113
|
if (typeof dataDir !== 'string') return
|
|
140
114
|
// Plugins stay silent: a failed append only loses one index entry.
|
|
141
115
|
const recorded = await recordToolEdits({ dataDir, directory: ctx.location.directory, sessionId: event.sessionID, tool: event.tool, input: event.input })
|
package/src/session-events.ts
CHANGED
|
@@ -110,9 +110,13 @@ export async function resolveSession({
|
|
|
110
110
|
type Message = Awaited<ReturnType<OpenCodeClient['message']['list']>>['data'][number]
|
|
111
111
|
type Session = Awaited<ReturnType<OpenCodeClient['session']['get']>>
|
|
112
112
|
|
|
113
|
+
// Long enough for most edits, commands and subagent prompts; huge writes stay short.
|
|
114
|
+
const TOOL_INPUT_LIMIT = 5000
|
|
115
|
+
|
|
113
116
|
function renderMessage(message: Message, options: { thinking?: boolean; verbose?: boolean }): string {
|
|
114
117
|
const time = new Date(message.time.created).toISOString()
|
|
115
|
-
if (message.type === 'user') return `##
|
|
118
|
+
if (message.type === 'user' || message.type === 'synthetic' || message.type === 'system') return `## ${message.type} ${time}\n\n${message.text}`
|
|
119
|
+
if (message.type === 'compaction' && message.status !== 'failed') return `## compaction ${time}\n\n${message.summary}`
|
|
116
120
|
if (message.type !== 'assistant') return `## ${message.type} ${time}\n\n${JSON.stringify(message)}`
|
|
117
121
|
const header = `## assistant ${time} (${message.agent}, ${message.model.providerID}/${message.model.id})`
|
|
118
122
|
const parts = message.content.map((part) => {
|
|
@@ -120,7 +124,7 @@ function renderMessage(message: Message, options: { thinking?: boolean; verbose?
|
|
|
120
124
|
if (part.type === 'reasoning') return options.thinking ? part.text : ''
|
|
121
125
|
if (part.type === 'tool') {
|
|
122
126
|
const state = part.state
|
|
123
|
-
const input = options.verbose ? JSON.stringify(state.input ?? {}) : JSON.stringify(state.input ?? {}).slice(0,
|
|
127
|
+
const input = options.verbose ? JSON.stringify(state.input ?? {}) : JSON.stringify(state.input ?? {}).slice(0, TOOL_INPUT_LIMIT)
|
|
124
128
|
const error = state.status === 'error' ? `\nerror: ${JSON.stringify(state).slice(0, 500)}` : ''
|
|
125
129
|
const output = options.verbose && state.status === 'completed' ? `\n${JSON.stringify(state.content)}` : ''
|
|
126
130
|
return `tool ${part.name} [${state.status}] ${input}${error}${output}`
|
|
@@ -158,7 +162,7 @@ function renderSession({ session, messages, thinking, verbose }: { session: Pick
|
|
|
158
162
|
}
|
|
159
163
|
|
|
160
164
|
// Transcripts: <dataDir>/transcripts/<sessionId>.md, the `session read` markdown
|
|
161
|
-
//
|
|
165
|
+
// with thinking but no tool outputs, so agents can search every session with rg.
|
|
162
166
|
// The file mtime is the session's time.updated: a file at least that new is current.
|
|
163
167
|
export function transcriptsDir({ dataDir }: { dataDir: string }): string {
|
|
164
168
|
return path.join(dataDir, 'transcripts')
|
|
@@ -172,7 +176,7 @@ export async function writeTranscript({ client, dataDir, session }: { client: Op
|
|
|
172
176
|
const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`
|
|
173
177
|
const written = await (async () => {
|
|
174
178
|
await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 })
|
|
175
|
-
await fs.promises.writeFile(temp, `${renderSession({ session, messages })}\n`, { mode: 0o600 })
|
|
179
|
+
await fs.promises.writeFile(temp, `${renderSession({ session, messages, thinking: true })}\n`, { mode: 0o600 })
|
|
176
180
|
await fs.promises.utimes(temp, session.time.updated / 1000, session.time.updated / 1000)
|
|
177
181
|
await fs.promises.rename(temp, file)
|
|
178
182
|
})().catch((cause) => new ConfigError({ reason: `Cannot write ${file}`, cause }))
|
package/src/sessions.ts
CHANGED
|
@@ -12,7 +12,6 @@ import * as orm from 'drizzle-orm'
|
|
|
12
12
|
import {
|
|
13
13
|
cliContext,
|
|
14
14
|
globalModel,
|
|
15
|
-
markerIsCurrent,
|
|
16
15
|
oc,
|
|
17
16
|
parseModel,
|
|
18
17
|
projectOf,
|
|
@@ -204,6 +203,7 @@ async function putInstructions(
|
|
|
204
203
|
agents,
|
|
205
204
|
parentSessionId,
|
|
206
205
|
scheduledTask,
|
|
206
|
+
disabled: bot.disabledPrompt,
|
|
207
207
|
})
|
|
208
208
|
const put = await oc(bot, 'instructions.entry.put', (client) =>
|
|
209
209
|
client.session.instructions.entry.put({ sessionID: sessionId, key: INSTRUCTION_KEY, value }),
|
|
@@ -274,7 +274,7 @@ export async function ensureSessionMarker(
|
|
|
274
274
|
const info = await oc(bot, `get session ${sessionId}`, (client) => client.session.get({ sessionID: sessionId }))
|
|
275
275
|
if (info instanceof Error) return info
|
|
276
276
|
const marker = readMarker(info.metadata)
|
|
277
|
-
if (marker &&
|
|
277
|
+
if (marker && marker.dataDir === bot.dataDir && marker.lockPort === bot.lockPort) return
|
|
278
278
|
// Instructions first: the marker means "set up", so a failure here is retried on the next input.
|
|
279
279
|
if (!marker) {
|
|
280
280
|
logger.log(`adopting legacy session ${sessionId} of thread ${thread.id}`)
|
|
@@ -304,7 +304,7 @@ export async function refreshCliContext(bot: Bot) {
|
|
|
304
304
|
}
|
|
305
305
|
const marker = readMarker(info.metadata)
|
|
306
306
|
if (!marker) continue
|
|
307
|
-
if (
|
|
307
|
+
if (marker.dataDir === bot.dataDir && marker.lockPort === bot.lockPort) continue
|
|
308
308
|
const metadata = { ...info.metadata, kimaki: { ...marker.fields, ...cliContext(bot) } }
|
|
309
309
|
const result = await oc(bot, 'refresh Kimaki CLI context', (client) => client.session.update({ sessionID: sessionId, metadata }))
|
|
310
310
|
if (result instanceof Error) return result
|