@zetaloop/chappie 0.3.2 → 0.4.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.
package/src/config.ts CHANGED
@@ -5,8 +5,8 @@ import * as z from "zod";
5
5
  const configSchema = z.object({
6
6
  ask: z.boolean().optional(),
7
7
  connect: z.string().min(1).optional(),
8
- latestWorkflow: z.boolean().optional(),
9
8
  listen: z.union([z.boolean(), z.number().int().min(1).max(65535)]).optional(),
9
+ sync: z.boolean().optional(),
10
10
  });
11
11
 
12
12
  export async function readConfig(agentDir: string) {
package/src/delivery.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import type { ToolResultMessage } from "@earendil-works/pi-ai";
2
+ import type { Source } from "./activity.ts";
2
3
  import {
3
4
  contentWithImageReferences,
4
5
  resourceDescriptors,
5
6
  } from "./resources.ts";
6
7
 
7
- export interface DeliveryRecord {
8
+ export interface DeliveryRecord extends Source {
8
9
  id: string;
9
- chatId: string;
10
10
  sessionId: string;
11
11
  cwd: string;
12
12
  toolResults: ToolResultMessage[];
@@ -43,6 +43,7 @@ export function deliveryContent(deliveries: DeliveryRecord[]) {
43
43
  type: "text" as const,
44
44
  text: JSON.stringify({
45
45
  deferredResult: delivery.id,
46
+ requestId: delivery.requestId,
46
47
  sessionId: delivery.sessionId,
47
48
  cwd: delivery.cwd,
48
49
  error: delivery.error,
package/src/history.ts ADDED
@@ -0,0 +1,123 @@
1
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ import * as z from "zod";
3
+ import type { Source } from "./activity.ts";
4
+ import { toolResultsContent } from "./delivery.ts";
5
+ import type { ProviderOutput } from "./provider.ts";
6
+ import { contentWithImageReferences, rememberImages } from "./resources.ts";
7
+
8
+ export const historyInput = z.object({
9
+ limit: z
10
+ .number()
11
+ .int()
12
+ .min(1)
13
+ .default(20)
14
+ .describe("Maximum number of history entries"),
15
+ before: z
16
+ .string()
17
+ .min(1)
18
+ .optional()
19
+ .describe("Read entries before this entry ID"),
20
+ after: z
21
+ .string()
22
+ .min(1)
23
+ .optional()
24
+ .describe("Read entries after this entry ID, starting with the earliest"),
25
+ });
26
+
27
+ export type HistoryRange = z.infer<typeof historyInput>;
28
+ export type HistoryResult = ReturnType<typeof historyResult>;
29
+
30
+ export const historyInstructions =
31
+ "When resuming work, read recent history to recover progress, then continue from the current request.";
32
+
33
+ export function historyResult(
34
+ branch: SessionEntry[],
35
+ sessionId: string,
36
+ { limit, before, after }: HistoryRange,
37
+ ) {
38
+ const start = after ? entryIndex(branch, after) + 1 : 0;
39
+ const end = before ? entryIndex(branch, before) : branch.length;
40
+ if (start > end) throw new Error("History after must precede before");
41
+ const entries = branch.slice(start, end).filter((entry) => {
42
+ switch (entry.type) {
43
+ case "message":
44
+ return (
45
+ entry.message.role !== "custom" ||
46
+ entry.message.customType !== "chappie.request"
47
+ );
48
+ case "custom_message":
49
+ return entry.customType !== "chappie.request";
50
+ case "custom":
51
+ return entry.customType === "chappie.notice";
52
+ case "compaction":
53
+ case "branch_summary":
54
+ return true;
55
+ default:
56
+ return false;
57
+ }
58
+ });
59
+ const selected = after ? entries.slice(0, limit) : entries.slice(-limit);
60
+ const sources = new Map<string, Source>();
61
+ for (const entry of branch) {
62
+ if (entry.type !== "message" || entry.message.role !== "assistant")
63
+ continue;
64
+ const message = entry.message as ProviderOutput["message"];
65
+ if (!message.chappie) continue;
66
+ for (const block of message.content) {
67
+ if (block.type === "toolCall") sources.set(block.id, message.chappie);
68
+ }
69
+ }
70
+ return {
71
+ count: selected.length,
72
+ hasMore: entries.length > selected.length,
73
+ content: selected.flatMap((entry) =>
74
+ entryContent(entry, sessionId, sources),
75
+ ),
76
+ };
77
+ }
78
+
79
+ function entryIndex(entries: SessionEntry[], id: string): number {
80
+ const index = entries.findIndex((entry) => entry.id === id);
81
+ if (index === -1)
82
+ throw new Error(`History entry ${id} is not on the current branch`);
83
+ return index;
84
+ }
85
+
86
+ function entryContent(
87
+ entry: SessionEntry,
88
+ sessionId: string,
89
+ sources: Map<string, Source>,
90
+ ): ReturnType<typeof toolResultsContent> {
91
+ const record: Record<string, unknown> = { ...entry };
92
+ const message = entry.type === "message" ? entry.message : entry;
93
+ if (!("content" in message)) {
94
+ return [{ type: "text", text: JSON.stringify(record) }];
95
+ }
96
+ const { content, ...metadata } = message;
97
+ if (entry.type === "message") {
98
+ const source =
99
+ entry.message.role === "toolResult"
100
+ ? sources.get(entry.message.toolCallId)
101
+ : undefined;
102
+ record.message = { ...metadata, ...(source ? { chappie: source } : {}) };
103
+ } else delete record.content;
104
+ const header = { type: "text" as const, text: JSON.stringify(record) };
105
+ if (entry.type === "message" && entry.message.role === "toolResult") {
106
+ rememberImages(sessionId, entry.message.content);
107
+ return [header, ...toolResultsContent([entry.message], sessionId)];
108
+ }
109
+ const blocks =
110
+ typeof content === "string"
111
+ ? [{ type: "text" as const, text: content }]
112
+ : content;
113
+ return [
114
+ header,
115
+ ...blocks.flatMap((block): ReturnType<typeof toolResultsContent> => {
116
+ if (block.type === "text" || block.type === "image") {
117
+ rememberImages(sessionId, [block]);
118
+ return contentWithImageReferences(sessionId, [block]);
119
+ }
120
+ return [{ type: "text", text: JSON.stringify(block) }];
121
+ }),
122
+ ];
123
+ }
@@ -1,6 +1,8 @@
1
1
  Chappie connects this ChatGPT conversation to Pi sessions on one or more devices. Resume work with init using the task's Pi sessionId, including in a new chat or branch. When only a project or session name is known, find its device/cwd/name in sessions, then call init. If the target is absent or ambiguous, ask the user to resolve it. Pi sessions appear while the chappie/chatgpt provider is selected.
2
2
 
3
- init sets this chat's default Pi session. A session tool also establishes a default on first use: sessionId selects its target, or omitting it selects the first online session with no saved bindings. These sessions may already contain work. Once a default exists, another tool's sessionId selects only that call's target. Defaults survive broker restarts, and several chats can share one Pi session.
3
+ init sets this chat's default Pi session. An execution tool also establishes a default on first use: sessionId selects its target, or omitting it selects the first online session with no saved bindings. These sessions may already contain work. Once a default exists, another tool's sessionId selects only that call's target. Defaults survive broker restarts, and several chats can share one Pi session. When resuming work after explicit or implicit initialization, read recent history to recover progress, then continue from the current request.
4
+
5
+ Use initialization.name, derived from the initialization request suffix, to identify yourself during coordination.
4
6
 
5
7
  The active model is this existing ChatGPT conversation. A Pi tool that starts another chappie/chatgpt agent has no ChatGPT conversation to attach to and will wait indefinitely. Subagents targeting another configured model keep that provider's normal behavior.
6
8
 
@@ -13,3 +15,5 @@ Use chat for assistant messages in Pi, including progress, explanations, and res
13
15
  transfer pairs files from ChatGPT with Pi destination paths in order. Omit files to return resource links for Pi paths or chappie:// image references. Relative paths use the Pi working directory; overwrite: true replaces existing targets. The host may request confirmation when retrieving exported bytes.
14
16
 
15
17
  Tool results identify the executing Pi sessionId and cwd. A shell command can access another directory without changing its Pi session. structuredContent.text includes the complete text, new Pi input, webAnswer, and deferred results; images and resources are native content blocks. Continue from received results rather than repeating work. Host deadlines include queueing and execution; use local persistent processes for longer work.
18
+
19
+ Use history to read the current Pi branch with original entry IDs and timestamps, including chat activity records. Assistant messages and tool results expose their originating chatId and requestId in message.chappie; activity records include the same fields. Parallel executions can share a chatId and workflow ID. Workflow timing helps explain differences in their task goals. History defaults to the last 20 entries; before pages backward and after pages forward. History is a record of past work, separate from new Pi input. It leaves a reading notice and returns its content only to the caller.
package/src/ipc.ts CHANGED
@@ -17,7 +17,9 @@ import type {
17
17
  SlashCommandInfo,
18
18
  ToolInfo,
19
19
  } from "@earendil-works/pi-coding-agent";
20
+ import type { Activity } from "./activity.ts";
20
21
  import type { DeliveryRecord } from "./delivery.ts";
22
+ import type { HistoryRange, HistoryResult } from "./history.ts";
21
23
  import type { ResourceData } from "./resources.ts";
22
24
 
23
25
  const defaultPort = 24274;
@@ -57,6 +59,7 @@ export type SessionResult =
57
59
  toolResults: ToolResultMessage[];
58
60
  inputs: SessionInput[];
59
61
  }
62
+ | { history: HistoryResult; cwd: string }
60
63
  | { resource: ResourceData }
61
64
  | { error: string };
62
65
 
@@ -69,12 +72,21 @@ export type SessionMessage =
69
72
  export type BrokerMessage =
70
73
  | { type: "synced"; id: number; sessionId: string }
71
74
  | { type: "stored"; id: string }
72
- | { type: "notice"; sessionId: string; message: string }
75
+ | { type: "notice"; sessionId: string; message: string; activity?: Activity }
73
76
  | { type: "inspect"; id: number; sessionId: string }
77
+ | {
78
+ type: "history";
79
+ id: number;
80
+ sessionId: string;
81
+ range: HistoryRange;
82
+ chatId: string;
83
+ requestId?: string;
84
+ }
74
85
  | {
75
86
  type: "chat";
76
87
  id: number;
77
88
  chatId: string;
89
+ requestId?: string;
78
90
  sessionId: string;
79
91
  text: string;
80
92
  }
@@ -82,6 +94,7 @@ export type BrokerMessage =
82
94
  type: "call";
83
95
  id: number;
84
96
  chatId: string;
97
+ requestId?: string;
85
98
  sessionId: string;
86
99
  calls: ToolCall[];
87
100
  }
package/src/provider.ts CHANGED
@@ -9,10 +9,11 @@ import {
9
9
  type StreamOptions,
10
10
  type ToolCall,
11
11
  } from "@earendil-works/pi-ai";
12
+ import type { Source } from "./activity.ts";
12
13
 
13
14
  export class ProviderOutput {
14
15
  readonly stream: AssistantMessageEventStream;
15
- readonly message: AssistantMessage;
16
+ readonly message: AssistantMessage & { chappie?: Source };
16
17
  readonly finished: Promise<void>;
17
18
  #resolveFinished: () => void;
18
19
  #removeAbort?: () => void;
package/src/server.ts CHANGED
@@ -4,6 +4,7 @@ import * as z from "zod";
4
4
  import packageJson from "../package.json" with { type: "json" };
5
5
  import type { Broker } from "./broker.ts";
6
6
  import { deliveryContent } from "./delivery.ts";
7
+ import { historyInput } from "./history.ts";
7
8
  import {
8
9
  answerContent,
9
10
  answerInput,
@@ -31,6 +32,9 @@ const outputSchema = z.object({
31
32
  ),
32
33
  });
33
34
 
35
+ const syncInstructions =
36
+ "On conflicting activity, start sync and verify with your own initialization code. Keep codes private. Verification returns a new code to the coordinator; ordinary tools and initialization stay locked. Discuss through chat and history, including after failed verification. The coordinator decides who continues, their tasks, and who exits, including retaining only one execution. Acknowledge the decision; if asked to exit, leave a chat handoff and end your response. Only the coordinator releases after decisions and exits are confirmed.";
37
+
34
38
  const questionTemplate = "ui://chappie/question.html";
35
39
  const questionSchema = outputSchema.extend({ question: questionOutput });
36
40
  const toolAnnotations = {
@@ -54,26 +58,30 @@ export function createServer(broker: Broker): McpServer {
54
58
  version: packageJson.version,
55
59
  },
56
60
  {
57
- instructions: broker.askEnabled
58
- ? `${instructions}\n\n${questionInstructions}`
59
- : instructions,
61
+ instructions: [
62
+ instructions,
63
+ ...(broker.askEnabled ? [questionInstructions] : []),
64
+ ...(broker.syncEnabled ? [syncInstructions] : []),
65
+ ].join("\n\n"),
60
66
  },
61
67
  );
62
68
 
63
69
  function handle<Args, Result>(
64
70
  callback: (args: Args, context: RequestContext) => Promise<Result>,
71
+ communication = false,
65
72
  ) {
66
- return async (args: Args, context: RequestContext): Promise<Result> => {
67
- const signal = await broker.workflow(
68
- requestChatId(context),
69
- context.mcpReq._meta?.["otunnel/requestId"],
73
+ return (args: Args, context: RequestContext): Promise<Result> =>
74
+ broker.run(
75
+ requireChatId(context),
76
+ (args as { sessionId?: string }).sessionId,
70
77
  context.mcpReq.signal,
78
+ (signal) =>
79
+ callback(args, {
80
+ ...context,
81
+ mcpReq: { ...context.mcpReq, signal },
82
+ }),
83
+ communication,
71
84
  );
72
- return callback(args, {
73
- ...context,
74
- mcpReq: { ...context.mcpReq, signal },
75
- });
76
- };
77
85
  }
78
86
 
79
87
  server.registerTool(
@@ -81,7 +89,7 @@ export function createServer(broker: Broker): McpServer {
81
89
  {
82
90
  title: "Connect to Pi",
83
91
  description:
84
- "Select this chat's default Pi session and return its environment and tool catalog. Use the task's sessionId to resume, or find it by cwd/name with sessions. For a task without a specified target, omit sessionId to reuse the default or select the first online, unbound session.",
92
+ "Select this chat's default Pi session and return its environment, tool catalog, and initialization name for coordination. Use the task's sessionId to resume, or find it by cwd/name with sessions. For a task without a specified target, omit sessionId to reuse the default or select the first online, unbound session. Read recent history when resuming work.",
85
93
  outputSchema,
86
94
  inputSchema: z.object({
87
95
  sessionId: z
@@ -123,7 +131,7 @@ export function createServer(broker: Broker): McpServer {
123
131
  },
124
132
  handle(async (args, context) => {
125
133
  const chatId = requireChatId(context);
126
- const { sessionId, cwd, inputs } = await broker.chat(
134
+ const { sessionId, cwd, inputs, initialization } = await broker.chat(
127
135
  chatId,
128
136
  args.sessionId,
129
137
  args.text,
@@ -133,9 +141,12 @@ export function createServer(broker: Broker): McpServer {
133
141
  return finishResult(
134
142
  broker,
135
143
  context,
136
- textResult({ sessionId, cwd }, inputs),
144
+ textResult(
145
+ { sessionId, cwd, ...(initialization ? { initialization } : {}) },
146
+ inputs,
147
+ ),
137
148
  );
138
- }),
149
+ }, true),
139
150
  );
140
151
 
141
152
  if (broker.askEnabled) {
@@ -158,7 +169,7 @@ export function createServer(broker: Broker): McpServer {
158
169
  _meta: { ui: { resourceUri: questionTemplate } },
159
170
  },
160
171
  handle(async ({ sessionId, ...input }, context) => {
161
- const question = await broker.ask(
172
+ const { initialization, ...question } = await broker.ask(
162
173
  requireChatId(context),
163
174
  sessionId,
164
175
  input,
@@ -167,6 +178,7 @@ export function createServer(broker: Broker): McpServer {
167
178
  );
168
179
  const result = await finishResult(broker, context, {
169
180
  content: [
181
+ ...(initialization ? textResult({ initialization }).content : []),
170
182
  {
171
183
  type: "text",
172
184
  text: `Question created. Call ask_assert({"questionId":"${question.id}"}) next.`,
@@ -226,7 +238,7 @@ export function createServer(broker: Broker): McpServer {
226
238
  annotations: toolAnnotations,
227
239
  _meta: { ui: { visibility: ["app"] }, "openai/widgetAccessible": true },
228
240
  },
229
- handle(async ({ questionId, answer, loaded = false }, context) => {
241
+ async ({ questionId, answer, loaded = false }, context) => {
230
242
  const question = await broker.answer(
231
243
  requireChatId(context),
232
244
  questionId,
@@ -244,7 +256,7 @@ export function createServer(broker: Broker): McpServer {
244
256
  content: [{ type: "text", text }],
245
257
  structuredContent: { text, question },
246
258
  };
247
- }),
259
+ },
248
260
  );
249
261
 
250
262
  server.registerResource(
@@ -308,7 +320,13 @@ export function createServer(broker: Broker): McpServer {
308
320
  broker,
309
321
  context,
310
322
  textResult(
311
- { session: inspected.session, tools: inspected.tools },
323
+ {
324
+ session: inspected.session,
325
+ tools: inspected.tools,
326
+ ...(inspected.initialization
327
+ ? { initialization: inspected.initialization }
328
+ : {}),
329
+ },
312
330
  inputs,
313
331
  ),
314
332
  );
@@ -356,6 +374,7 @@ export function createServer(broker: Broker): McpServer {
356
374
  result.sessionId,
357
375
  result.cwd,
358
376
  result.inputs,
377
+ result.initialization,
359
378
  ),
360
379
  );
361
380
  }),
@@ -396,12 +415,93 @@ export function createServer(broker: Broker): McpServer {
396
415
  result.sessionId,
397
416
  result.cwd,
398
417
  result.inputs,
418
+ result.initialization,
399
419
  ),
400
420
  );
401
421
  }),
402
422
  );
403
423
  }
404
424
 
425
+ server.registerTool(
426
+ "history",
427
+ {
428
+ title: "Session history",
429
+ description:
430
+ "Read recent Pi history with entry IDs and timestamps. Use before/after to page the current branch. History provides context for the current task. An explicit sessionId applies only to this read.",
431
+ inputSchema: historyInput.extend({
432
+ sessionId: z
433
+ .string()
434
+ .optional()
435
+ .describe("Pi session to read; defaults to this chat's session"),
436
+ }),
437
+ outputSchema,
438
+ annotations: toolAnnotations,
439
+ },
440
+ handle(async ({ sessionId, ...range }, context) => {
441
+ const { history, ...session } = await broker.history(
442
+ requireChatId(context),
443
+ sessionId,
444
+ range,
445
+ context.mcpReq._meta?.["otunnel/requestId"],
446
+ context.mcpReq.signal,
447
+ );
448
+ const { content, ...page } = history;
449
+ return formatResult({
450
+ content: [
451
+ ...textResult({
452
+ ...session,
453
+ history: page,
454
+ ...(broker.syncEnabled
455
+ ? { sync: broker.syncState(session.sessionId) }
456
+ : {}),
457
+ }).content,
458
+ ...content,
459
+ ],
460
+ });
461
+ }, true),
462
+ );
463
+
464
+ if (broker.syncEnabled) {
465
+ server.registerTool(
466
+ "sync",
467
+ {
468
+ title: "Synchronize Pi activity",
469
+ description:
470
+ "Pause conflicting activity. Verify your initialization code to coordinate; failed verification still permits discussion. The coordinator decides who continues, their tasks, who exits, and when to release.",
471
+ inputSchema: z.object({
472
+ action: z.enum(["start", "verify", "release"]),
473
+ code: z
474
+ .string()
475
+ .optional()
476
+ .describe(
477
+ "Initialization code for verification, or the verified code for release; omit when unavailable",
478
+ ),
479
+ sessionId: z
480
+ .string()
481
+ .optional()
482
+ .describe(
483
+ "Pi session to synchronize; defaults to this chat's session",
484
+ ),
485
+ }),
486
+ outputSchema,
487
+ annotations: toolAnnotations,
488
+ },
489
+ async ({ action, code, sessionId }, context) =>
490
+ formatResult(
491
+ textResult(
492
+ await broker.sync(
493
+ requireChatId(context),
494
+ sessionId,
495
+ action,
496
+ code,
497
+ context.mcpReq._meta?.["otunnel/requestId"],
498
+ context.mcpReq.signal,
499
+ ),
500
+ ),
501
+ ),
502
+ );
503
+ }
504
+
405
505
  server.registerTool(
406
506
  "sessions",
407
507
  {
@@ -420,12 +520,7 @@ export function createServer(broker: Broker): McpServer {
420
520
  handle(async (args, context) => {
421
521
  const chatId = requestChatId(context);
422
522
  const inputs = chatId
423
- ? await broker.inputs(
424
- chatId,
425
- args.sessionId,
426
- context.mcpReq._meta?.["otunnel/requestId"],
427
- context.mcpReq.signal,
428
- )
523
+ ? await broker.inputs(chatId, args.sessionId, context.mcpReq.signal)
429
524
  : [];
430
525
  const result = textResult(
431
526
  {
@@ -435,7 +530,7 @@ export function createServer(broker: Broker): McpServer {
435
530
  inputs,
436
531
  );
437
532
  return finishResult(broker, context, result);
438
- }),
533
+ }, true),
439
534
  );
440
535
 
441
536
  server.registerResource(
@@ -479,21 +574,32 @@ function textResult(
479
574
  async function finishResult<
480
575
  T extends { content: ReturnType<typeof toolResult>["content"] },
481
576
  >(broker: Broker, context: RequestContext, result: T) {
577
+ context.mcpReq.signal.throwIfAborted();
482
578
  const chatId = requestChatId(context);
483
- const deliveries = chatId ? await broker.deliveries(chatId) : [];
579
+ if (!broker.deliveryAllowed(context.mcpReq.signal))
580
+ return formatResult(result);
581
+ const deliveries = chatId ? broker.deliveries(chatId) : [];
484
582
  const answers = chatId ? broker.answers(chatId) : [];
485
583
  const content = [
486
584
  ...result.content,
487
585
  ...deliveryContent(deliveries),
488
586
  ...answerContent(answers),
489
587
  ];
490
- const structuredContent = {
491
- text: content
492
- .flatMap((block) => (block.type === "text" ? [block.text] : []))
493
- .join("\n"),
494
- };
495
588
  await broker.acknowledge(deliveries, answers, context.mcpReq.signal);
496
- return { ...result, content, structuredContent };
589
+ return formatResult({ ...result, content });
590
+ }
591
+
592
+ function formatResult<
593
+ T extends { content: ReturnType<typeof toolResult>["content"] },
594
+ >(result: T) {
595
+ return {
596
+ ...result,
597
+ structuredContent: {
598
+ text: result.content
599
+ .flatMap((block) => (block.type === "text" ? [block.text] : []))
600
+ .join("\n"),
601
+ },
602
+ };
497
603
  }
498
604
 
499
605
  function requestChatId(context: RequestContext): string | undefined {
package/src/session.ts CHANGED
@@ -13,7 +13,9 @@ import type {
13
13
  SessionEntry,
14
14
  } from "@earendil-works/pi-coding-agent";
15
15
  import { Text } from "@earendil-works/pi-tui";
16
+ import { type Activity, chatLabel, source } from "./activity.ts";
16
17
  import type { DeliveryRecord } from "./delivery.ts";
18
+ import { historyResult } from "./history.ts";
17
19
  import {
18
20
  type BrokerMessage,
19
21
  IpcClient,
@@ -27,7 +29,7 @@ import { readSessionResource, rememberImages } from "./resources.ts";
27
29
 
28
30
  type RemoteRequest = Extract<BrokerMessage, { type: "chat" | "call" }>;
29
31
 
30
- interface Notice {
32
+ interface Notice extends Activity {
31
33
  message: string;
32
34
  type: "info" | "warning" | "error";
33
35
  }
@@ -126,9 +128,17 @@ export class LocalSession {
126
128
  this.#pi.on("session_shutdown", () => this.close());
127
129
  }
128
130
 
129
- #notify(message: string, type: Notice["type"] = "info"): void {
131
+ #notify(
132
+ message: string,
133
+ type: Notice["type"] = "info",
134
+ activity: Activity = {},
135
+ ): void {
130
136
  if (this.#context)
131
- this.#pi.appendEntry<Notice>("chappie.notice", { message, type });
137
+ this.#pi.appendEntry<Notice>("chappie.notice", {
138
+ message,
139
+ type,
140
+ ...activity,
141
+ });
132
142
  }
133
143
 
134
144
  async start(output: ProviderOutput): Promise<void> {
@@ -261,7 +271,7 @@ export class LocalSession {
261
271
  if (
262
272
  message.sessionId === this.#context?.sessionManager.getSessionId()
263
273
  ) {
264
- this.#notify(message.message);
274
+ this.#notify(message.message, "info", message.activity);
265
275
  }
266
276
  break;
267
277
  case "inspect":
@@ -276,6 +286,26 @@ export class LocalSession {
276
286
  };
277
287
  });
278
288
  break;
289
+ case "history":
290
+ await this.#reply(message.id, message.sessionId, () => {
291
+ const context = this.#context;
292
+ if (!context) throw new Error("Chappie session is not available");
293
+ const history = historyResult(
294
+ context.sessionManager.getBranch(),
295
+ message.sessionId,
296
+ message.range,
297
+ );
298
+ this.#notify(
299
+ `${chatLabel(message)} read history: ${history.count} entries`,
300
+ "info",
301
+ {
302
+ event: "history",
303
+ ...source(message.chatId, message.requestId),
304
+ },
305
+ );
306
+ return { type: "result", id: message.id, cwd: context.cwd, history };
307
+ });
308
+ break;
279
309
  case "ackInputs":
280
310
  if (
281
311
  message.sessionId === this.#context?.sessionManager.getSessionId()
@@ -305,8 +335,12 @@ export class LocalSession {
305
335
  ? [...new Set(request.calls.map((call) => call.name))].join(", ")
306
336
  : "chat";
307
337
  this.#notify(
308
- `${name} cancelled for ChatGPT ${request.chatId.slice(-4)}: ${message.reason}`,
338
+ `${name} cancelled for ${chatLabel(request)}: ${message.reason}`,
309
339
  "warning",
340
+ {
341
+ event: "cancelled",
342
+ ...source(request.chatId, request.requestId),
343
+ },
310
344
  );
311
345
  if (queued !== -1) {
312
346
  this.#queue.splice(queued, 1);
@@ -370,6 +404,7 @@ export class LocalSession {
370
404
  }
371
405
 
372
406
  this.#queue.shift();
407
+ output.message.chappie = source(request.chatId, request.requestId);
373
408
  this.#active = {
374
409
  request,
375
410
  session: this.#description(),
@@ -431,7 +466,7 @@ export class LocalSession {
431
466
  if (active.cancelled !== undefined) {
432
467
  const delivery: DeliveryRecord = {
433
468
  id: randomUUID(),
434
- chatId: active.request.chatId,
469
+ ...source(active.request.chatId, active.request.requestId),
435
470
  sessionId: active.session.id,
436
471
  cwd: active.session.cwd,
437
472
  toolResults: active.toolResults,
@@ -515,7 +550,14 @@ export class LocalSession {
515
550
  await connection.send({ type: "delivery", delivery });
516
551
  await completion.promise;
517
552
  this.#deliveries.delete(delivery.id);
518
- this.#notify(`Result saved for ChatGPT ${delivery.chatId.slice(-4)}`);
553
+ this.#notify(
554
+ `Result saved for ChatGPT ${delivery.chatId.slice(-4)}`,
555
+ "info",
556
+ {
557
+ event: "result_saved",
558
+ ...source(delivery.chatId, delivery.requestId),
559
+ },
560
+ );
519
561
  } finally {
520
562
  this.#stores.delete(delivery.id);
521
563
  }