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.
Files changed (71) hide show
  1. package/dist/bot.d.ts +3 -5
  2. package/dist/bot.js +3 -9
  3. package/dist/bot.js.map +1 -1
  4. package/dist/builtin-skills.d.ts +3 -0
  5. package/dist/builtin-skills.js +29 -0
  6. package/dist/builtin-skills.js.map +1 -1
  7. package/dist/builtin-skills.test.js +73 -1
  8. package/dist/builtin-skills.test.js.map +1 -1
  9. package/dist/cli/bot.d.ts +1 -1
  10. package/dist/cli/bot.js +8 -4
  11. package/dist/cli/bot.js.map +1 -1
  12. package/dist/cli-lock.e2e.test.js +1 -1
  13. package/dist/event-loop.d.ts +1 -2
  14. package/dist/event-loop.js +2 -2
  15. package/dist/event-loop.js.map +1 -1
  16. package/dist/main.d.ts +2 -1
  17. package/dist/main.js +2 -2
  18. package/dist/main.js.map +1 -1
  19. package/dist/markdown/markdown.test.js +10 -0
  20. package/dist/markdown/markdown.test.js.map +1 -1
  21. package/dist/markdown/render-markdown.js +3 -1
  22. package/dist/markdown/render-markdown.js.map +1 -1
  23. package/dist/plugin/index.js +17 -44
  24. package/dist/plugin/index.js.map +1 -1
  25. package/dist/session-events.js +9 -5
  26. package/dist/session-events.js.map +1 -1
  27. package/dist/sessions.js +4 -3
  28. package/dist/sessions.js.map +1 -1
  29. package/dist/system-prompt.d.ts +3 -15
  30. package/dist/system-prompt.js +52 -77
  31. package/dist/system-prompt.js.map +1 -1
  32. package/dist/test/harness.d.ts +1 -2
  33. package/dist/test/harness.js +1 -2
  34. package/dist/test/harness.js.map +1 -1
  35. package/dist/test/replay.js +0 -1
  36. package/dist/test/replay.js.map +1 -1
  37. package/dist/thread-reducer.d.ts +0 -3
  38. package/dist/thread-reducer.fixtures.test.js +2 -2
  39. package/dist/thread-reducer.fixtures.test.js.map +1 -1
  40. package/dist/thread-reducer.js +4 -32
  41. package/dist/thread-reducer.js.map +1 -1
  42. package/dist/thread-reducer.test.js +0 -31
  43. package/dist/thread-reducer.test.js.map +1 -1
  44. package/dist/tunnel.d.ts +9 -0
  45. package/dist/tunnel.js +117 -28
  46. package/dist/tunnel.js.map +1 -1
  47. package/dist/tunnel.test.js +81 -5
  48. package/dist/tunnel.test.js.map +1 -1
  49. package/package.json +4 -4
  50. package/skills/kimaki-tunnel/SKILL.md +3 -2
  51. package/src/bot.ts +7 -14
  52. package/src/builtin-skills.test.ts +74 -1
  53. package/src/builtin-skills.ts +33 -0
  54. package/src/cli/bot.ts +7 -5
  55. package/src/cli-lock.e2e.test.ts +1 -1
  56. package/src/event-loop.ts +1 -4
  57. package/src/main.ts +5 -4
  58. package/src/markdown/markdown.test.ts +11 -0
  59. package/src/markdown/render-markdown.ts +3 -1
  60. package/src/plugin/index.ts +18 -44
  61. package/src/session-events.ts +8 -4
  62. package/src/sessions.ts +3 -3
  63. package/src/system-prompt.ts +56 -80
  64. package/src/test/harness.ts +0 -3
  65. package/src/test/replay.ts +0 -1
  66. package/src/thread-reducer.fixtures.test.ts +2 -2
  67. package/src/thread-reducer.test.ts +0 -31
  68. package/src/thread-reducer.ts +4 -36
  69. package/src/tunnel.test.ts +81 -5
  70. package/src/tunnel.ts +124 -25
  71. 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 { Goke } from 'goke'
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
- messageTool: Boolean(options.messageTool),
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
- ...(options.messageTool ? ['--message-tool'] : []),
255
+ ...disabledPrompt.flatMap((section) => ['--disable-prompt', section]),
254
256
  ...(options.noAnalytics ? ['--no-analytics'] : []),
255
257
  ...(options.machineName ? ['--machine-name', options.machineName] : []),
256
258
  ] })
@@ -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, messageTool } })
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
- // `kimaki --message-tool`: agents also reply through send_message; Discord hides their plain text.
72
- messageTool?: boolean
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, messageTool: options.messageTool ?? false, onRunEnded: voiceCalls.runEnded, onTranscriptDue })
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
- messageTool: options.messageTool ?? false,
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
- const text = slice(source, node)
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))
@@ -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 { MESSAGE_MODE_INSTRUCTIONS, MESSAGE_TOOL, MESSAGE_TOOL_DESCRIPTION, MESSAGE_TOOL_INPUT, turnContext } from '../system-prompt.ts'
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
- // metadata.kimaki of the session, or of the thread's root for a subagent (`root: false`).
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 { fields: current, root: true }
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
- const root = await marker(session.parentID)
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
- // send_message only for root sessions of a bot started with --message-tool.
78
- // Every request kind that carries the tool list gets the same tools and system
79
- // text, so the prompt cache keeps hitting and no other session ever sees the tool.
80
- async function applyMessageMode(event: { sessionID: string; tools: Record<string, unknown>; system: Array<{ type: 'text'; text: string }> }) {
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
- const info = await applyMessageMode(event)
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?.fields.get('dataDir')
129
- const lockPort = info?.fields.get('lockPort')
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))?.fields.get('dataDir')
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 })
@@ -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 `## user ${time}\n\n${message.text}`
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, 500)
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
- // (no thinking, no tool outputs) so agents can search every session with rg.
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 && markerIsCurrent(bot, marker)) return
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 (markerIsCurrent(bot, marker)) continue
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