@cmodjs/core 1.6.0 → 2.0.1

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 (136) hide show
  1. package/docs/commands.md +7 -7
  2. package/docs/dependencies.md +1 -1
  3. package/docs/index.md +12 -11
  4. package/docs/jobs.md +12 -16
  5. package/docs/mod.md +111 -36
  6. package/docs/permissions.md +14 -18
  7. package/docs/session.md +171 -82
  8. package/docs/state.md +5 -5
  9. package/docs/testing.md +57 -41
  10. package/docs/ui.md +9 -9
  11. package/jobs/permissions/decide-permission.d.ts.map +1 -1
  12. package/jobs/permissions/decide-permission.js +1 -1
  13. package/jobs/prompt.d.ts +1 -3
  14. package/jobs/prompt.d.ts.map +1 -1
  15. package/jobs/prompt.js +2 -21
  16. package/jobs/slash-command.d.ts +1 -1
  17. package/jobs/slash-command.d.ts.map +1 -1
  18. package/jobs/slash-command.js +1 -1
  19. package/jobs/status-line.d.ts +0 -1
  20. package/jobs/status-line.d.ts.map +1 -1
  21. package/jobs/status-line.js +3 -4
  22. package/mod.d.ts +53 -105
  23. package/mod.d.ts.map +1 -1
  24. package/mod.js +18 -80
  25. package/package.json +1 -1
  26. package/records.d.ts +1 -1
  27. package/records.js +1 -1
  28. package/register.d.ts.map +1 -1
  29. package/register.js +8 -5
  30. package/runtime/before.d.ts +11 -4
  31. package/runtime/before.d.ts.map +1 -1
  32. package/runtime/before.js +131 -67
  33. package/runtime/claude.d.ts +6 -1
  34. package/runtime/claude.d.ts.map +1 -1
  35. package/runtime/conversations.d.ts.map +1 -1
  36. package/runtime/conversations.js +82 -38
  37. package/runtime/deadline.d.ts +17 -0
  38. package/runtime/deadline.d.ts.map +1 -1
  39. package/runtime/deadline.js +34 -0
  40. package/runtime/dependencies.d.ts +2 -0
  41. package/runtime/dependencies.d.ts.map +1 -1
  42. package/runtime/dependencies.js +12 -0
  43. package/runtime/grants.d.ts.map +1 -1
  44. package/runtime/grants.js +13 -8
  45. package/runtime/hooks.d.ts +15 -11
  46. package/runtime/hooks.d.ts.map +1 -1
  47. package/runtime/hooks.js +8 -165
  48. package/runtime/lifecycle.d.ts +4 -2
  49. package/runtime/lifecycle.d.ts.map +1 -1
  50. package/runtime/lifecycle.js +96 -62
  51. package/runtime/permission-check.d.ts +15 -0
  52. package/runtime/permission-check.d.ts.map +1 -0
  53. package/runtime/permission-check.js +37 -0
  54. package/runtime/sessions.d.ts +35 -5
  55. package/runtime/sessions.d.ts.map +1 -1
  56. package/runtime/sessions.js +304 -82
  57. package/runtime/state.d.ts.map +1 -1
  58. package/runtime/state.js +14 -11
  59. package/runtime/tool-calls.d.ts +0 -1
  60. package/runtime/tool-calls.d.ts.map +1 -1
  61. package/runtime/tool-calls.js +0 -19
  62. package/session/context/turn/message/instruction-file.d.ts +9 -0
  63. package/session/context/turn/message/instruction-file.d.ts.map +1 -0
  64. package/session/context/turn/message/instruction-file.js +4 -0
  65. package/session/context/turn/message/message.d.ts +6 -4
  66. package/session/context/turn/message/message.d.ts.map +1 -1
  67. package/session/context/turn/message/message.js +12 -8
  68. package/session/context/turn/message/sender.d.ts +2 -1
  69. package/session/context/turn/message/sender.d.ts.map +1 -1
  70. package/session/context/turn/message/sender.js +4 -1
  71. package/session/context/turn/message/tool-call.d.ts +5 -0
  72. package/session/context/turn/message/tool-call.d.ts.map +1 -1
  73. package/session/context/turn/message/tool-result.d.ts +6 -1
  74. package/session/context/turn/message/tool-result.d.ts.map +1 -1
  75. package/session/context/turn/turn.d.ts +7 -0
  76. package/session/context/turn/turn.d.ts.map +1 -1
  77. package/session/conversation.d.ts +10 -3
  78. package/session/conversation.d.ts.map +1 -1
  79. package/session/conversation.js +14 -5
  80. package/session/goal/goal.d.ts +1 -1
  81. package/session/goal/goal.d.ts.map +1 -1
  82. package/session/goal/task.d.ts +2 -2
  83. package/session/goal/task.d.ts.map +1 -1
  84. package/session/session.d.ts +361 -26
  85. package/session/session.d.ts.map +1 -1
  86. package/session/session.js +40 -0
  87. package/src/jobs/permissions/decide-permission.ts +2 -1
  88. package/src/jobs/prompt.ts +3 -20
  89. package/src/jobs/slash-command.ts +2 -2
  90. package/src/jobs/status-line.ts +4 -7
  91. package/src/mod.ts +52 -147
  92. package/src/records.ts +1 -1
  93. package/src/register.ts +8 -5
  94. package/src/runtime/before.ts +140 -66
  95. package/src/runtime/claude.ts +5 -1
  96. package/src/runtime/conversations.ts +75 -37
  97. package/src/runtime/deadline.ts +38 -0
  98. package/src/runtime/dependencies.ts +12 -0
  99. package/src/runtime/grants.ts +11 -7
  100. package/src/runtime/hooks.ts +24 -163
  101. package/src/runtime/lifecycle.ts +110 -77
  102. package/src/runtime/permission-check.ts +47 -0
  103. package/src/runtime/sessions.ts +334 -91
  104. package/src/runtime/state.ts +15 -8
  105. package/src/runtime/tool-calls.ts +0 -19
  106. package/src/session/context/turn/message/instruction-file.ts +13 -0
  107. package/src/session/context/turn/message/message.ts +17 -11
  108. package/src/session/context/turn/message/sender.ts +5 -2
  109. package/src/session/context/turn/message/tool-call.ts +3 -0
  110. package/src/session/context/turn/message/tool-result.ts +6 -1
  111. package/src/session/context/turn/turn.ts +4 -0
  112. package/src/session/conversation.ts +21 -8
  113. package/src/session/goal/goal.ts +1 -1
  114. package/src/session/goal/task.ts +2 -2
  115. package/src/session/session.ts +227 -24
  116. package/src/testing/claude-code.ts +23 -0
  117. package/src/testing/fake-claude.ts +5 -1
  118. package/src/testing/fake-sessions.ts +38 -24
  119. package/src/testing.ts +88 -19
  120. package/testing/claude-code.d.ts +12 -0
  121. package/testing/claude-code.d.ts.map +1 -0
  122. package/testing/claude-code.js +10 -0
  123. package/testing/fake-claude.d.ts +4 -0
  124. package/testing/fake-claude.d.ts.map +1 -1
  125. package/testing/fake-claude.js +4 -1
  126. package/testing/fake-sessions.d.ts +8 -6
  127. package/testing/fake-sessions.d.ts.map +1 -1
  128. package/testing/fake-sessions.js +35 -20
  129. package/testing.d.ts +38 -9
  130. package/testing.d.ts.map +1 -1
  131. package/testing.js +58 -8
  132. package/docs/hooks.md +0 -259
  133. package/session/context/turn/message/document.d.ts +0 -7
  134. package/session/context/turn/message/document.d.ts.map +0 -1
  135. package/session/context/turn/message/document.js +0 -1
  136. package/src/session/context/turn/message/document.ts +0 -8
@@ -1,15 +1,20 @@
1
1
  import { ContextEnd, ContextStart } from '../session/context/context.js';
2
- import { Message } from '../session/context/turn/message/message.js';
2
+ import { Message, noFiles } from '../session/context/turn/message/message.js';
3
3
  import { Note } from '../session/context/turn/message/note.js';
4
- import { Person } from '../session/context/turn/message/sender.js';
4
+ import { Person, senderOf } from '../session/context/turn/message/sender.js';
5
+ import { ToolCall } from '../session/context/turn/message/tool-call.js';
5
6
  import { ToolResult } from '../session/context/turn/message/tool-result.js';
6
7
  import { TurnEnd, TurnStart } from '../session/context/turn/turn.js';
8
+ import { searchLimit } from '../session/conversation.js';
7
9
  import { GoalChange } from '../session/goal/goal.js';
8
10
  import { TaskEnd, TaskStart } from '../session/goal/task.js';
9
- import { Session } from '../session/session.js';
11
+ import { beforeEvents, processEvents, Session, sessionEvents } from '../session/session.js';
10
12
  import { matches, sentMatcher } from '../utils/matcher.js';
11
- import { foldAnswers } from './before.js';
13
+ import { beforeAnswerKeys, foldAnswers, foldBefore, methodOf, takenAnswer } from './before.js';
12
14
  import { cmodPluginName } from './dependencies.js';
15
+ import { messageOf } from '../utils/text.js';
16
+ import { listenerOf, PassedTimeout, withinTime } from './deadline.js';
17
+ import { callFiles } from './hooks.js';
13
18
  export const everySession = '*';
14
19
  export const conversationMethods = {
15
20
  sessions: 'cmod:sessions',
@@ -27,10 +32,10 @@ export const conversationMethods = {
27
32
  changed: 'cmod:changed',
28
33
  before: 'cmod:before',
29
34
  };
30
- const actPermissions = { send: 'prompt', startTask: 'tasks', usage: 'conversation', clients: 'conversation' };
35
+ const actPermissions = { send: 'prompt', startTask: 'tasks', usage: 'conversation', surfaces: 'conversation', abort: 'prompt' };
31
36
  const startsTurns = ['CronCreate'];
32
37
  const ownSchedule = ['CronDelete', 'CronList'];
33
- export const beforePermission = (event) => (event === 'toolCall' ? 'tools' : event === TaskStart || event === TaskEnd ? 'tasks' : 'prompt');
38
+ export const beforePermission = (event) => (event === 'toolCall' ? 'tools' : event === TaskStart ? 'tasks' : 'prompt');
34
39
  export const isAct = (action) => action === 'callTool' || action in actPermissions;
35
40
  export function toolPermission(tool) {
36
41
  return startsTurns.includes(tool) ? 'prompt' : ownSchedule.includes(tool) ? undefined : 'tools';
@@ -38,11 +43,10 @@ export function toolPermission(tool) {
38
43
  export function actPermission(action, input) {
39
44
  return action === 'callTool' ? (toolPermission(String(input?.name)) ?? 'tools') : actPermissions[action];
40
45
  }
41
- const asked = (action) => `cmod: ${action} runs only on a session's own main loop, and Claude Code gives a subagent's tasks no way to start a task or run a tool.`;
42
46
  export async function usageOf(claude) {
43
47
  const [model, usage] = await Promise.all([claude.session.model(), claude.session.usage()]);
44
48
  const { tokens, window, percent } = usage.context;
45
- return { model, context: { ...(tokens === undefined ? {} : { tokens }), window, ...(percent === undefined ? {} : { percent }) }, ...(usage.cost === undefined ? {} : { cost: { usd: usage.cost.usd } }) };
49
+ return { model, contextWindow: { ...(tokens === undefined ? {} : { tokenCount: tokens }), size: window, ...(percent === undefined ? {} : { percent }) }, ...(usage.cost === undefined ? {} : { cost: { usd: usage.cost.usd } }) };
46
50
  }
47
51
  export function payloadOf(change) {
48
52
  if (change.type === Message)
@@ -96,10 +100,10 @@ export function isInScope(scope, session, change, isUnder) {
96
100
  if (scope.session !== undefined && scope.session !== session)
97
101
  return false;
98
102
  const task = taskOfChange(change);
103
+ if (scope.isAgent === true)
104
+ return (change.type === TaskStart || change.type === TaskEnd ? change.task.parent : task) === scope.task;
99
105
  if (scope.task !== undefined && (task === undefined || !isUnder(task, scope.task)))
100
106
  return false;
101
- if (scope.isGoal === true && change.type !== GoalChange && change.type !== TaskStart && (task === undefined || task === scope.task))
102
- return false;
103
107
  if (scope.context !== undefined && (contextOfChange(change) !== scope.context || ownerOfChange(change) !== scope.task))
104
108
  return false;
105
109
  if (scope.turn === undefined)
@@ -108,7 +112,7 @@ export function isInScope(scope, session, change, isUnder) {
108
112
  return change.message.turn === scope.turn;
109
113
  return (change.type === TurnStart || change.type === TurnEnd) && change.turn.prompt.id === scope.turn;
110
114
  }
111
- export function createSessionsClient({ name, claude, report, isGranted }) {
115
+ export function createSessionsClient({ name, claude, report, isGranted, hookOn, routeOn, permissionRoutes }) {
112
116
  const call = (method, input) => claude.cmod.call({ to: cmodPluginName, method, input });
113
117
  const listening = new Map();
114
118
  const deciding = new Map();
@@ -149,11 +153,9 @@ export function createSessionsClient({ name, claude, report, isGranted }) {
149
153
  if (change.type === TaskStart)
150
154
  parents.get(session)?.set(change.task.id, change.task.parent);
151
155
  };
152
- const add = (into, scope, event, matcherOrListener, maybeListener, onChange) => {
153
- const listener = (maybeListener ?? matcherOrListener);
154
- const matcher = maybeListener === undefined ? undefined : matcherOrListener;
156
+ const add = (into, scope, event, method, args, onChange) => {
155
157
  const key = keyOf(scope);
156
- const entry = { scope, event, matcher, listener };
158
+ const entry = { scope, event, method, ...listenerOf(name, method, args) };
157
159
  const set = into.get(key) ?? new Set();
158
160
  set.add(entry);
159
161
  into.set(key, set);
@@ -173,6 +175,12 @@ export function createSessionsClient({ name, claude, report, isGranted }) {
173
175
  },
174
176
  };
175
177
  };
178
+ const invoke = (method, options, run, takes = []) => withinTime(claude.clock, method, options, run).catch((error) => {
179
+ if (error instanceof PassedTimeout && !method.startsWith('before'))
180
+ return undefined;
181
+ report(`session:${method}`, `The ${method} listener failed`, error);
182
+ return options.onFailure === 'refuse' && takes.includes('refuse') ? { refuse: `${name}: ${messageOf(error)}` } : undefined;
183
+ });
176
184
  const combined = async (session, event, change, isInProject) => {
177
185
  const askers = [...(deciding.get(session) ?? []), ...(isInProject ? (deciding.get(everySession) ?? []) : [])]
178
186
  .filter((entry) => entry.event === event)
@@ -181,31 +189,71 @@ export function createSessionsClient({ name, claude, report, isGranted }) {
181
189
  return undefined;
182
190
  if (entry.matcher !== undefined && !matches(payloadOf(latest), entry.matcher))
183
191
  return undefined;
184
- const answer = await entry.listener(payloadFor(session, latest));
185
- if (answer === undefined || isGranted(beforePermission(event)))
192
+ const payload = await payloadFor(session, latest);
193
+ const answer = await invoke(entry.method, entry.options, async (time) => takenAnswer(entry.method, beforeAnswerKeys[event], await entry.listener(payload, time)), beforeAnswerKeys[event]);
194
+ if (answer === undefined || answer === null || (typeof answer === 'object' && 'refuse' in answer) || isGranted(beforePermission(event)))
186
195
  return answer;
187
- throw new Error(`Its before(${event}) answer changes nothing until ${beforePermission(event)} is turned on for ${name} in /mods.`);
196
+ throw new Error(`Its ${entry.method} answer changes nothing until ${beforePermission(event)} is turned on for ${name} in /mods.`);
188
197
  });
189
- return foldAnswers(event, change, askers, (error) => report(`before:${event}`, `The before(${event}) listener failed`, error));
198
+ return foldBefore(event, change, askers, (error) => report(`session:${methodOf('before', event)}`, `The ${methodOf('before', event)} listener failed`, error));
199
+ };
200
+ const eventMethods = (scopeOf, hasBefore, ended) => {
201
+ const method = (into, named, event) => (...args) => {
202
+ if (ended !== undefined)
203
+ throw new Error(`${name}: ${ended} has ended, so its ${named} never fires. Listen on its Session.`);
204
+ return add(into, scopeOf(), event, named, args, subscribe);
205
+ };
206
+ return Object.fromEntries([
207
+ ...Object.entries(sessionEvents).map(([named, event]) => [`on${named}`, method(listening, `on${named}`, event)]),
208
+ ...(hasBefore ? beforeEvents.map((named) => [`before${named}`, method(deciding, `before${named}`, sessionEvents[named])]) : []),
209
+ ]);
210
+ };
211
+ let folders;
212
+ const withFiles = async (message) => {
213
+ if (message.type !== ToolCall && message.type !== ToolResult)
214
+ return message;
215
+ folders ??= Promise.all([claude.env.home(), claude.env.configHome()]).then(([home, configRoot]) => ({ home, configRoot }));
216
+ return { ...message, files: await callFiles(name, claude, folders, { tool: message.name, input: message.input }, message.cwd) };
190
217
  };
191
- const recordPart = (scope) => ({
192
- search: (text, options = {}) => call(conversationMethods.search, { text, ...scope, ...optionsOf(options) }),
193
- on: (event, matcherOrListener, maybeListener) => add(listening, scope, event, matcherOrListener, maybeListener, subscribe),
194
- before: (event, matcherOrListener, maybeListener) => add(deciding, scope, event, matcherOrListener, maybeListener, subscribe),
218
+ const messagesFrom = async (method, input) => Promise.all((await call(method, input)).map(withFiles));
219
+ const searched = (request, matcher) => ({
220
+ async *[Symbol.asyncIterator]() {
221
+ let before = request['before'];
222
+ for (;;) {
223
+ const page = await messagesFrom(conversationMethods.search, { ...request, ...(before === undefined ? {} : { before }) });
224
+ for (const message of page)
225
+ if (matcher === undefined || matches(message, matcher))
226
+ yield message;
227
+ const last = page.at(-1);
228
+ if (last === undefined || page.length < searchLimit)
229
+ return;
230
+ before = { session: last.session, id: last.id };
231
+ }
232
+ },
233
+ });
234
+ const recordPart = (scope, ended) => ({
235
+ search: (text, matcher) => searched({ text, ...scope }, matcher),
236
+ ...eventMethods(() => scope, true, ended),
195
237
  });
196
238
  const turnOf = (scope, data) => {
197
239
  const task = data.prompt.task ?? scope.task;
198
240
  return {
199
241
  ...data,
200
- ...recordPart({ ...scope, ...(task === undefined ? {} : { task }), turn: data.prompt.id }),
201
- messages: () => call(conversationMethods.messages, { session: scope.session, turn: data.prompt.id, ...(task === undefined ? {} : { task }) }),
242
+ ...recordPart({ ...scope, ...(task === undefined ? {} : { task }), turn: data.prompt.id }, data.closedAt === undefined ? undefined : `turn ${data.prompt.id}`),
243
+ messages: () => messagesFrom(conversationMethods.messages, { session: scope.session, turn: data.prompt.id, ...(task === undefined ? {} : { task }) }),
244
+ abort: async () => {
245
+ if (data.closedAt !== undefined)
246
+ throw new Error(`${name}: turn ${data.prompt.id} has ended, so there is nothing to abort.`);
247
+ const session = scope.session ?? current.id;
248
+ await act(session, 'abort', { turn: data.prompt.id }, task === undefined ? undefined : { session, task });
249
+ },
202
250
  };
203
251
  };
204
252
  const contextOf = (scope, data) => {
205
253
  const owned = { ...(scope.session === undefined ? {} : { session: scope.session }), ...(data.task === undefined ? {} : { task: data.task }) };
206
254
  return {
207
255
  ...data,
208
- ...recordPart({ ...owned, context: data.number }),
256
+ ...recordPart({ ...owned, context: data.number }, data.closedAt === undefined ? undefined : `context ${data.number}`),
209
257
  turns: async () => (await call(conversationMethods.turns, { ...owned, context: data.number })).map((turn) => turnOf(owned, turn)),
210
258
  };
211
259
  };
@@ -218,22 +266,27 @@ export function createSessionsClient({ name, claude, report, isGranted }) {
218
266
  owner: ownerOf(session, data),
219
267
  ...recordPart({ session, task: data.id }),
220
268
  tasks: async () => (await call(conversationMethods.tasks, { session, task: data.id })).map((task) => taskOf(session, task)),
221
- messages: () => call(conversationMethods.messages, { session, task: data.id }),
269
+ messages: () => messagesFrom(conversationMethods.messages, { session, task: data.id }),
222
270
  });
223
- const goalOf = (scope, data) => ({
224
- ...data,
225
- ...recordPart({ ...scope, isGoal: true }),
226
- tasks: async () => (await call(conversationMethods.tasks, { session: scope.session, ...(scope.task === undefined ? {} : { task: scope.task }) })).map((task) => taskOf(scope.session ?? current.id, task)),
271
+ const goalOf = (where) => ({
272
+ search: (text, matcher) => searched({ text, ...where() }, matcher),
273
+ ...eventMethods(() => ({ ...where(), isGoal: true }), true),
274
+ text: async () => (await call(conversationMethods.goal, where())).text,
275
+ tasks: async () => {
276
+ const { session = current.id, task } = where();
277
+ return (await call(conversationMethods.tasks, { session, ...(task === undefined ? {} : { task }) })).map((each) => taskOf(session, each));
278
+ },
227
279
  update: async ({ text }) => {
228
280
  if (text.trim() === '')
229
281
  throw new Error(`${name}: a goal needs text.`);
230
- await call(conversationMethods.updateGoal, { session: scope.session ?? current.id, ...(scope.task === undefined ? {} : { task: scope.task }), text });
282
+ const { session = current.id, task } = where();
283
+ await call(conversationMethods.updateGoal, { session, ...(task === undefined ? {} : { task }), text });
231
284
  },
232
285
  });
233
- const payloadFor = (session, change) => {
286
+ const payloadFor = async (session, change) => {
234
287
  const scope = { session };
235
288
  if (change.type === Message)
236
- return change.message;
289
+ return withFiles(change.message);
237
290
  if ('turn' in change)
238
291
  return turnOf(scope, change.turn);
239
292
  if ('context' in change)
@@ -242,7 +295,8 @@ export function createSessionsClient({ name, claude, report, isGranted }) {
242
295
  return taskOf(session, change.task);
243
296
  if ('usage' in change)
244
297
  return change.usage;
245
- return goalOf({ session, ...(change.goal.task === undefined ? {} : { task: change.goal.task }) }, { text: change.goal.text });
298
+ const { task } = change.goal;
299
+ return goalOf(() => ({ session, ...(task === undefined ? {} : { task }) }));
246
300
  };
247
301
  const act = (session, action, input, owned) => call(conversationMethods.act, { session: owned?.session ?? session, action, input, ...(owned === undefined ? {} : { task: owned.task }) });
248
302
  const sendHere = async ({ type, text, as }) => {
@@ -258,26 +312,15 @@ export function createSessionsClient({ name, claude, report, isGranted }) {
258
312
  if ('drop' in submitted && typeof submitted.drop === 'string')
259
313
  throw new Error(`${name}: Claude Code dropped the prompt: ${submitted.drop}`);
260
314
  };
261
- const startHere = async (task) => {
262
- const started = await claude.agent.spawn({
263
- prompt: task.prompt,
264
- ...(task.name === undefined ? {} : { description: task.name, name: task.name }),
265
- ...(task.agentType === undefined ? {} : { subagentType: task.agentType }),
266
- ...(task.model === undefined ? {} : { model: task.model }),
267
- ...(task.cwd === undefined ? {} : { cwd: task.cwd }),
268
- });
269
- if (started.deny !== undefined)
270
- throw new Error(`${name}: Claude Code refused the task: ${started.deny}`);
271
- return taskOf(current.id, { id: started.agentId ?? '', title: task.name ?? '', agentType: task.agentType ?? 'general-purpose', model: started.model, openedBy: '', openedAt: new Date().toISOString() });
272
- };
273
- const clientsHere = async () => [...(await claude.session.surfaces())];
315
+ const startHere = async (task) => taskOf(current.id, await spawnTask(claude, task).catch((error) => Promise.reject(new Error(`${name}: ${messageOf(error)}`))));
316
+ const surfacesHere = async () => [...(await claude.session.surfaces())];
274
317
  const sessionOf = (read, owned) => {
275
318
  const id = () => read().id;
276
319
  const isHere = () => owned === undefined && id() === current.id;
277
- const scope = () => (owned === undefined ? { session: id() } : { session: owned.session, task: owned.task });
320
+ const scope = () => (owned === undefined ? { session: id(), isAgent: true } : { session: owned.session, task: owned.task, isAgent: true });
278
321
  const where = () => (owned === undefined ? { session: id() } : { session: owned.session, task: owned.task });
279
- const turnsOf = async (options = {}) => (await call(conversationMethods.turns, { ...where(), ...options })).map((turn) => turnOf(scope(), turn));
280
- const contextsOf = async () => (await call(conversationMethods.contexts, where())).map((context) => contextOf(scope(), context));
322
+ const turnsOf = async (options = {}) => (await call(conversationMethods.turns, { ...where(), ...options })).map((turn) => turnOf(where(), turn));
323
+ const contextsOf = async () => (await call(conversationMethods.contexts, where())).map((context) => contextOf(where(), context));
281
324
  const session = {
282
325
  get id() {
283
326
  return read().id;
@@ -309,10 +352,9 @@ export function createSessionsClient({ name, claude, report, isGranted }) {
309
352
  get forkedFrom() {
310
353
  return read().forkedFrom;
311
354
  },
312
- search: (text, options = {}) => recordPart(scope()).search(text, options),
313
- on: (event, matcherOrListener, maybeListener) => recordPart(scope()).on(event, matcherOrListener, maybeListener),
314
- before: (event, matcherOrListener, maybeListener) => recordPart(scope()).before(event, matcherOrListener, maybeListener),
315
- goal: async () => goalOf(scope(), await call(conversationMethods.goal, where())),
355
+ search: (text, matcher) => recordPart(scope()).search(text, matcher),
356
+ ...eventMethods(scope, true),
357
+ goal: goalOf(where),
316
358
  context: async () => {
317
359
  const all = await contextsOf();
318
360
  const last = all.at(-1);
@@ -326,9 +368,9 @@ export function createSessionsClient({ name, claude, report, isGranted }) {
326
368
  return last?.closedAt === undefined ? last : undefined;
327
369
  },
328
370
  turns: turnsOf,
329
- messages: (options = {}) => call(conversationMethods.messages, { ...where(), ...options }),
371
+ messages: (options = {}) => messagesFrom(conversationMethods.messages, { ...where(), ...options }),
330
372
  usage: async () => (isHere() ? usageOf(claude) : act(id(), 'usage', null, owned)),
331
- clients: async () => (isHere() ? clientsHere() : act(id(), 'clients', null, owned)),
373
+ surfaces: async () => (isHere() ? surfacesHere() : act(id(), 'surfaces', null, owned)),
332
374
  async send(message) {
333
375
  if (message.text.trim() === '')
334
376
  throw new Error(`${name}: send needs text.`);
@@ -336,21 +378,18 @@ export function createSessionsClient({ name, claude, report, isGranted }) {
336
378
  return sendHere(message);
337
379
  await act(id(), 'send', message, owned);
338
380
  },
381
+ };
382
+ if (owned !== undefined)
383
+ return session;
384
+ return Object.assign(session, {
339
385
  async startTask(task) {
340
- if (owned !== undefined)
341
- throw new Error(asked('startTask'));
342
386
  if (isHere())
343
387
  return startHere(task);
344
388
  const started = await act(id(), 'startTask', task);
345
389
  return taskOf(id(), started);
346
390
  },
347
- async callTool(toolCall) {
348
- if (owned !== undefined)
349
- throw new Error(asked('callTool'));
350
- return act(id(), 'callTool', toolCall);
351
- },
352
- };
353
- return session;
391
+ callTool: async (toolCall) => (await withFiles(await act(id(), 'callTool', toolCall))),
392
+ });
354
393
  };
355
394
  const ownerOf = (session, data) => {
356
395
  const parent = session === current.id ? current : known.get(session);
@@ -366,9 +405,79 @@ export function createSessionsClient({ name, claude, report, isGranted }) {
366
405
  lastAt: data.closedAt ?? data.openedAt,
367
406
  }), { session, task: data.id });
368
407
  };
369
- const currentSession = sessionOf(() => current);
370
- const every = recordPart({});
371
- const answerBefore = async (session, event, change, isInProject) => (isGranted('conversation') ? combined(session, event, change, isInProject) : undefined);
408
+ const processListeners = new Map();
409
+ const raise = async (method, payload) => {
410
+ const heard = [...(processListeners.get(method) ?? [])].filter(({ matcher }) => payload['task'] === undefined && (matcher === undefined || matches(payload, matcher)));
411
+ if (!method.startsWith('before')) {
412
+ await Promise.all(heard.map(({ listener, options }) => invoke(method, options, (time) => listener(payload, time))));
413
+ return undefined;
414
+ }
415
+ const failing = method === 'beforePermissionRequest' ? { onFailure: 'refuse' } : {};
416
+ const takes = answerKeys[method.slice('before'.length)] ?? [];
417
+ const askers = heard.map(({ listener, options }) => () => invoke(method, { ...options, ...failing }, async (time) => takenAnswer(method, takes, await listener(payload, time)), takes));
418
+ return foldAnswers(method, takes, ['refuse'], undefined, askers, (error) => report(`session:${method}`, `The ${method} listener failed`, error));
419
+ };
420
+ const commandRun = async (e) => {
421
+ const source = (await claude.command.list()).find((command) => command.name === e.command)?.source;
422
+ return { session: current.id, cwd: current.cwd, name: e.command, args: e.args, ...defined({ source }), from: senderOf(e.origin, false) };
423
+ };
424
+ const commandListeners = { beforeRun: new Set(), onRun: new Set() };
425
+ for (const [method, held] of Object.entries(commandListeners))
426
+ processListeners.set(method, held);
427
+ routeOn?.('command.run', async (e, next) => {
428
+ if (commandListeners.beforeRun.size === 0 && commandListeners.onRun.size === 0)
429
+ return next(e);
430
+ const run = await commandRun(e);
431
+ const answer = commandListeners.beforeRun.size === 0 ? undefined : await raise('beforeRun', run);
432
+ if (typeof answer?.['refuse'] === 'string')
433
+ return { text: answer['refuse'] };
434
+ const changes = answer !== undefined && isGranted('prompt') ? answer : undefined;
435
+ if (answer !== undefined && changes === undefined)
436
+ report('session:beforeRun', 'The beforeRun listener failed', new Error(`${name}: its answer changes a command, which needs the prompt permission.`));
437
+ if (typeof changes?.['result'] === 'string')
438
+ return { text: changes['result'], ...(typeof changes['exitCode'] === 'number' ? { exitCode: changes['exitCode'] } : {}) };
439
+ const args = typeof changes?.['args'] === 'string' ? changes['args'] : e.args;
440
+ const ran = await next(args === e.args ? e : { ...e, args });
441
+ void raise('onRun', { ...run, args });
442
+ return typeof changes?.['note'] === 'string' ? { ...ran, context: [...(ran.context ?? []), changes['note']] } : ran;
443
+ });
444
+ const listenTo = (method, named) => {
445
+ if (hookOn === undefined || routeOn === undefined)
446
+ throw new Error(`${name}: ${method} works only in a mod's own session.`);
447
+ const isBefore = method.startsWith('before');
448
+ if (named === 'PermissionCheck' || named === 'PermissionRequest') {
449
+ const routes = permissionRoutes?.();
450
+ if (routes !== undefined)
451
+ return routes(method, { raise, hookOn, routeOn, current: () => current });
452
+ return named === 'PermissionCheck' ? routeOn('tool.check', (e, next) => next(e)) : hookOn('PermissionRequest', () => undefined);
453
+ }
454
+ const event = processEvents[named];
455
+ const hook = (isBefore ? event.before : undefined) ?? event.on;
456
+ return hookOn(hook, async (input) => {
457
+ const payload = processPayloadOf(named, input);
458
+ if (!isBefore) {
459
+ if (named !== 'SessionStart' || payload['source'] !== 'compact')
460
+ void raise(method, payload);
461
+ return undefined;
462
+ }
463
+ const answer = await raise(method, payload);
464
+ return answer === undefined ? undefined : hookAnswerOf(named, answer);
465
+ });
466
+ };
467
+ const listen = (method, args) => {
468
+ const entry = listenerOf(name, method, args);
469
+ const held = processListeners.get(method) ?? new Set();
470
+ if (!processListeners.has(method)) {
471
+ listenTo(method, method.replace(/^(on|before)/, ''));
472
+ processListeners.set(method, held);
473
+ }
474
+ held.add(entry);
475
+ return { dispose: () => void held.delete(entry) };
476
+ };
477
+ const processMethods = () => Object.fromEntries(Object.entries(processEvents).flatMap(([named, event]) => [`on${named}`, ...('before' in event ? [`before${named}`] : [])]).map((method) => [method, (...args) => listen(method, args)]));
478
+ const currentSession = Object.assign(sessionOf(() => current), processMethods());
479
+ const every = eventMethods(() => ({}), false);
480
+ const answerBefore = async (session, event, change, isInProject) => (session === current.id || isGranted('conversation') ? combined(session, event, change, isInProject) : undefined);
372
481
  return {
373
482
  sessions: {
374
483
  get current() {
@@ -383,23 +492,26 @@ export function createSessionsClient({ name, claude, report, isGranted }) {
383
492
  current = { ...current, title: data.title };
384
493
  return currentSession;
385
494
  }),
386
- search: (text, options = {}) => call(conversationMethods.search, { text, ...optionsOf(options), ...(options.running === undefined ? {} : { running: options.running }), ...(options.since === undefined ? {} : { since: options.since }), ...(options.everyProject === undefined ? {} : { everyProject: options.everyProject }) }),
387
- on: every.on,
388
- before: every.before,
495
+ search: (text, matcher, options = {}) => searched({ text, ...searchOptionsOf(options) }, matcher),
496
+ ...every,
389
497
  },
390
498
  changed(session, change, isInProject) {
391
499
  remember(session, change);
500
+ let payload;
392
501
  for (const key of isInProject ? [session, everySession] : [session]) {
393
- for (const { scope, event, matcher, listener } of listening.get(key) ?? []) {
502
+ for (const { scope, event, method, matcher, listener, options } of listening.get(key) ?? []) {
394
503
  if (!isHeardBy(event, matcher, change) || !isInScope(scope, session, change, isUnderIn(session)))
395
504
  continue;
396
- void Promise.resolve()
397
- .then(() => listener(payloadFor(session, change)))
398
- .catch((error) => report(`session:${event}`, `The ${event} listener failed`, error));
505
+ payload ??= payloadFor(session, change);
506
+ void payload
507
+ .then((heard) => invoke(method, options, (time) => listener(heard, time)))
508
+ .catch((error) => report(`session:${method}`, `The ${method} listener failed`, error));
399
509
  }
400
510
  }
401
511
  },
402
512
  answerBefore,
513
+ raise,
514
+ listen,
403
515
  follow(next) {
404
516
  const moved = next.id !== current.id && current.id !== '';
405
517
  const previous = current.id;
@@ -422,12 +534,122 @@ export function createSessionsClient({ name, claude, report, isGranted }) {
422
534
  },
423
535
  };
424
536
  }
425
- export function resultOf(where, answered) {
426
- const base = { id: '', type: ToolResult, at: new Date().toISOString(), session: where.session, context: where.context, ...(where.turn === undefined ? {} : { turn: where.turn.prompt.id }), callId: '' };
537
+ export const recordOf = (value) => (typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {});
538
+ export const defined = (fields) => Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
539
+ const placementOf = (input) => defined({ session: input['session_id'], task: input['agent_id'], agentType: input['agent_type'], cwd: input['cwd'], permissionMode: input['permission_mode'] });
540
+ const mcpServerOf = (server) => (server === undefined ? undefined : { name: String(recordOf(server)['name']), source: String(recordOf(server)['source']) });
541
+ const modelChangeOf = (input) => ({
542
+ model: input['to_model'],
543
+ previous: input['from_model'],
544
+ requested: input['requested_model'] ?? undefined,
545
+ source: input['source'],
546
+ contextTokenCount: input['context_tokens'],
547
+ isCacheWarm: input['prompt_cache_warm'],
548
+ cacheTtl: input['cache_ttl'],
549
+ cacheWriteUsd: input['estimated_cache_write_usd'],
550
+ pricing: input['pricing'],
551
+ });
552
+ const answerKeys = {
553
+ Run: ['args', 'note', 'result', 'exitCode', 'refuse'],
554
+ ProjectSetup: [],
555
+ SessionStart: ['note', 'title', 'reloadSkills', 'firstPrompt'],
556
+ ModelChange: ['ask', 'refuse'],
557
+ WorktreeCreate: ['result'],
558
+ Elicitation: ['refuse'],
559
+ ElicitationResponse: ['refuse'],
560
+ PermissionCheck: ['allow', 'ask', 'refuse'],
561
+ PermissionRequest: ['allow', 'input', 'updates', 'refuse', 'interrupt'],
562
+ PermissionDenial: ['retry'],
563
+ ToolBatch: ['note'],
564
+ TodoCreate: ['refuse'],
565
+ TodoComplete: ['refuse'],
566
+ ReplyDisplay: ['text'],
567
+ };
568
+ const todoOf = (input) => ({ id: input['task_id'], subject: input['task_subject'], description: input['task_description'], teammate: input['teammate_name'] });
569
+ const processFields = {
570
+ ProjectSetup: (input) => ({ trigger: input['trigger'] }),
571
+ SessionStart: (input) => ({
572
+ source: input['source'],
573
+ model: input['model'],
574
+ title: input['session_title'] || undefined,
575
+ idleSeconds: input['seconds_since_last_response'],
576
+ contextTokenCount: input['context_tokens'],
577
+ isCacheWarm: input['prompt_cache_likely_expired'] === undefined ? undefined : input['prompt_cache_likely_expired'] !== true,
578
+ cacheWriteUsd: input['estimated_cache_write_usd'],
579
+ }),
580
+ SessionEnd: (input) => ({ reason: input['reason'] === 'prompt_input_exit' ? 'exit' : input['reason'] }),
581
+ ModelChange: modelChangeOf,
582
+ CwdChange: (input) => ({ cwd: input['new_cwd'] ?? input['cwd'], previous: input['old_cwd'] }),
583
+ DirectoryAdd: (input) => ({ directory: input['directory'], source: input['source'] === 'slash_command' ? 'command' : 'sdk' }),
584
+ WorktreeCreate: (input) => ({ name: input['name'] }),
585
+ WorktreeRemove: (input) => ({ path: input['worktree_path'] }),
586
+ Notification: (input) => ({ text: input['message'], title: input['title'], type: input['notification_type'] }),
587
+ Elicitation: (input) => ({ id: input['elicitation_id'], server: input['mcp_server_name'], text: input['message'], mode: input['mode'], url: input['url'], schema: input['requested_schema'] }),
588
+ ElicitationResponse: (input) => ({ id: input['elicitation_id'], server: input['mcp_server_name'], mode: input['mode'], action: input['action'], content: input['content'] }),
589
+ PermissionRequest: (input) => ({ name: input['tool_name'], input: recordOf(input['tool_input']), suggestions: input['permission_suggestions'] ?? [], mcpServer: mcpServerOf(input['mcp_server']) }),
590
+ PermissionDenial: (input) => ({ name: input['tool_name'], input: recordOf(input['tool_input']), callId: input['tool_use_id'], reason: input['reason'], mcpServer: mcpServerOf(input['mcp_server']) }),
591
+ ToolBatch: (input) => ({
592
+ calls: (Array.isArray(input['tool_calls']) ? input['tool_calls'] : []).map((call) => {
593
+ const fields = recordOf(call);
594
+ return defined({ callId: fields['tool_use_id'], name: fields['tool_name'], input: recordOf(fields['tool_input']), output: fields['tool_response'] });
595
+ }),
596
+ }),
597
+ TodoCreate: todoOf,
598
+ TodoComplete: todoOf,
599
+ ReplyDisplay: (input) => ({ replyId: input['message_id'], index: input['index'], isLast: input['final'], text: input['delta'] }),
600
+ };
601
+ export async function spawnTask(claude, task) {
602
+ if (task.title.trim() === '')
603
+ throw new Error('a task needs a title.');
604
+ const started = await claude.agent.spawn({
605
+ prompt: task.prompt,
606
+ description: task.title,
607
+ ...(task.name === undefined ? {} : { name: task.name }),
608
+ ...(task.agentType === undefined ? {} : { subagentType: task.agentType }),
609
+ ...(task.model === undefined ? {} : { model: task.model }),
610
+ ...(task.cwd === undefined ? {} : { cwd: task.cwd }),
611
+ });
612
+ if (started.deny !== undefined)
613
+ throw new Error(`Claude Code refused the task: ${started.deny}`);
614
+ if (started.agentId === undefined)
615
+ throw new Error('Claude Code answered without starting a task.');
616
+ return { id: started.agentId, title: task.title, agentType: task.agentType ?? 'general-purpose', model: started.model, openedBy: '', openedAt: new Date().toISOString() };
617
+ }
618
+ export function processPayloadOf(named, input) {
619
+ const fields = recordOf(input);
620
+ return { ...placementOf(fields), ...defined(processFields[named](fields)) };
621
+ }
622
+ function hookAnswerOf(named, answer) {
623
+ const refuse = typeof answer['refuse'] === 'string' ? answer['refuse'] : undefined;
624
+ const note = typeof answer['note'] === 'string' ? [answer['note']] : undefined;
625
+ if (named === 'SessionStart')
626
+ return defined({ additionalContext: note, sessionTitle: answer['title'], reloadSkills: answer['reloadSkills'] === true ? true : undefined, initialUserMessage: answer['firstPrompt'] });
627
+ if (named === 'ModelChange') {
628
+ if (refuse !== undefined)
629
+ return { permissionDecision: 'deny', permissionDecisionReason: refuse };
630
+ return typeof answer['ask'] === 'string' ? { permissionDecision: 'ask', permissionDecisionReason: answer['ask'] } : undefined;
631
+ }
632
+ if (named === 'WorktreeCreate')
633
+ return defined({ worktreePath: answer['result'] });
634
+ if (named === 'PermissionDenial')
635
+ return answer['retry'] === true ? { retry: true } : undefined;
636
+ if (named === 'ToolBatch')
637
+ return defined({ additionalContext: note });
638
+ if (named === 'ReplyDisplay')
639
+ return defined({ displayContent: answer['text'] });
640
+ return refuse === undefined ? undefined : { block: refuse };
641
+ }
642
+ export function resultOf(where, call, answered) {
643
+ const base = { id: '', type: ToolResult, at: new Date().toISOString(), session: where.session, context: where.context, ...(where.turn === undefined ? {} : { turn: where.turn.prompt.id }), callId: '', name: call.name, input: call.input, cwd: call.cwd, files: noFiles };
427
644
  if (answered.deny !== undefined)
428
- return { ...base, text: answered.deny, isError: true };
429
- return { ...base, text: answered.text ?? JSON.stringify(answered.result), isError: answered.isError === true };
645
+ return { ...base, output: answered.deny, isError: true };
646
+ return { ...base, output: answered.result ?? answered.text, isError: answered.isError === true };
430
647
  }
431
- function optionsOf(options) {
432
- return { ...(options.limit === undefined ? {} : { limit: options.limit }), ...(options.before === undefined ? {} : { before: { session: options.before.session, id: options.before.id } }) };
648
+ function searchOptionsOf({ running, since, everyProject, before }) {
649
+ return {
650
+ ...(running === undefined ? {} : { running }),
651
+ ...(since === undefined ? {} : { since }),
652
+ ...(everyProject === undefined ? {} : { everyProject }),
653
+ ...(before === undefined ? {} : { before: { session: before.session, id: before.id } }),
654
+ };
433
655
  }
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/runtime/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAQ3C,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CACvD,CAAA;AAED,MAAM,MAAM,QAAQ,CAAC,KAAK,SAAS,MAAM,IAAI;IAC3C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,OAAO,IAAI,IAAI,CAAA;IACf,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnC,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,CAAC,cAAc,CAAC,CAAC,QAAQ,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvH,CAAA;AAQD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAIvE;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAO9H;AAED,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,YAAY,GAAG,QAAQ,CAAC,KAAK,CAAC,CA6G9J"}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/runtime/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAQ3C,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CACvD,CAAA;AAED,MAAM,MAAM,QAAQ,CAAC,KAAK,SAAS,MAAM,IAAI;IAC3C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,OAAO,IAAI,IAAI,CAAA;IACf,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnC,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,CAAC,cAAc,CAAC,CAAC,QAAQ,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvH,CAAA;AAQD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAIvE;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAO9H;AAED,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,YAAY,GAAG,QAAQ,CAAC,KAAK,CAAC,CAoH9J"}
package/runtime/state.js CHANGED
@@ -46,17 +46,18 @@ export function createState({ name, initial, session, root, claude, report, chan
46
46
  const transcriptsKey = `${name}.$transcripts.`;
47
47
  const forgetEndedSessions = async (current) => {
48
48
  const stored = await claude.store.keys();
49
- for (const recorded of stored.filter((key) => key.startsWith(transcriptsKey))) {
50
- const owner = recorded.slice(transcriptsKey.length);
51
- if (owner === current)
52
- continue;
49
+ const ended = async (recorded) => {
53
50
  const transcript = await claude.store.get(recorded);
54
- if (typeof transcript === 'string' && (await claude.fs.exists(transcript)))
55
- continue;
56
- for (const value of stored.filter((key) => key.startsWith(`${name}.`) && key.endsWith(`.sessions.${owner}`)))
57
- await claude.store.delete(value);
51
+ return typeof transcript !== 'string' || !(await claude.fs.exists(transcript));
52
+ };
53
+ const owners = stored.filter((key) => key.startsWith(transcriptsKey) && key !== `${transcriptsKey}${current}`);
54
+ await Promise.all(owners.map(async (recorded) => {
55
+ if (!(await ended(recorded)))
56
+ return;
57
+ const owner = recorded.slice(transcriptsKey.length);
58
+ await Promise.all(stored.filter((key) => key.startsWith(`${name}.`) && key.endsWith(`.sessions.${owner}`)).map((value) => claude.store.delete(value)));
58
59
  await claude.store.delete(recorded);
59
- }
60
+ }));
60
61
  };
61
62
  const loadGroups = async (only, next) => {
62
63
  const loaded = [];
@@ -114,8 +115,10 @@ export function createState({ name, initial, session, root, claude, report, chan
114
115
  }),
115
116
  moveTo: (nextRoot) => inTurn(async () => (nextRoot === projectRoot ? undefined : loadGroups(['project'], { root: nextRoot, session: sessionId }))),
116
117
  switchSession: (nextSession, source, transcript) => inTurn(async () => {
117
- await claude.store.set(`${transcriptsKey}${nextSession}`, transcript);
118
- await forgetEndedSessions(nextSession);
118
+ if (Object.keys(declared.session).length > 0) {
119
+ await claude.store.set(`${transcriptsKey}${nextSession}`, transcript);
120
+ void forgetEndedSessions(nextSession).catch((error) => report({ id: 'state:sessions', message: 'Could not forget the session values of ended sessions', error }));
121
+ }
119
122
  if (nextSession === sessionId)
120
123
  return;
121
124
  if (source === 'fork')
@@ -6,7 +6,6 @@ export type Agent = {
6
6
  };
7
7
  export type ToolCalls = {
8
8
  agentOf(toolUseId: string | undefined): Promise<Agent>;
9
- cwdOf(toolUseId: string): string | undefined;
10
9
  };
11
10
  export declare const reservedKeys: readonly string[];
12
11
  export declare function toolInputOf(envelope: object): Record<string, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"tool-calls.d.ts","sourceRoot":"","sources":["../src/runtime/tool-calls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,MAAM,KAAK,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5D,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,CAAA;IACtD,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAC7C,CAAA;AAED,eAAO,MAAM,YAAY,EAAE,SAAS,MAAM,EAAkD,CAAA;AAE5F,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAErE;AAMD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,CA8CnE"}
1
+ {"version":3,"file":"tool-calls.d.ts","sourceRoot":"","sources":["../src/runtime/tool-calls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,MAAM,KAAK,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5D,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,CAAA;CACvD,CAAA;AAED,eAAO,MAAM,YAAY,EAAE,SAAS,MAAM,EAAkD,CAAA;AAE5F,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAErE;AAID,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,CA8BnE"}