@netnodeag/kraftwerk 0.57.3 → 0.58.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 (84) hide show
  1. package/README.md +85 -2
  2. package/dist/acp.js +2 -1
  3. package/dist/cli/journal.d.ts +10 -0
  4. package/dist/cli/journal.js +36 -0
  5. package/dist/cli/knowledge.js +17 -0
  6. package/dist/cli/kraftwerk.js +6 -0
  7. package/dist/cli/projects.js +3 -3
  8. package/dist/cli/repos.js +1 -1
  9. package/dist/cli/trash.d.ts +2 -0
  10. package/dist/cli/trash.js +70 -0
  11. package/dist/cli/vibeables.js +3 -3
  12. package/dist/inspector/agent-status.d.ts +21 -0
  13. package/dist/inspector/agent-status.js +25 -0
  14. package/dist/inspector/agents.d.ts +5 -0
  15. package/dist/inspector/agents.js +10 -1
  16. package/dist/inspector/attention.d.ts +48 -0
  17. package/dist/inspector/attention.js +66 -0
  18. package/dist/inspector/channels.js +2 -1
  19. package/dist/inspector/chat/sessions.d.ts +52 -1
  20. package/dist/inspector/chat/sessions.js +160 -27
  21. package/dist/inspector/chat/store.d.ts +2 -0
  22. package/dist/inspector/chat/store.js +5 -1
  23. package/dist/inspector/files.d.ts +87 -0
  24. package/dist/inspector/files.js +316 -0
  25. package/dist/inspector/git.js +3 -1
  26. package/dist/inspector/journal.d.ts +31 -0
  27. package/dist/inspector/journal.js +72 -0
  28. package/dist/inspector/knowledge.d.ts +2 -0
  29. package/dist/inspector/knowledge.js +7 -0
  30. package/dist/inspector/projects.d.ts +1 -1
  31. package/dist/inspector/projects.js +5 -2
  32. package/dist/inspector/repos.js +2 -1
  33. package/dist/inspector/runs.d.ts +1 -1
  34. package/dist/inspector/runs.js +3 -2
  35. package/dist/inspector/server.js +111 -1
  36. package/dist/inspector/skills.d.ts +1 -1
  37. package/dist/inspector/skills.js +3 -2
  38. package/dist/inspector/status-line.d.ts +14 -0
  39. package/dist/inspector/status-line.js +44 -0
  40. package/dist/inspector/trash.d.ts +37 -0
  41. package/dist/inspector/trash.js +148 -0
  42. package/dist/inspector/vibeables.d.ts +1 -1
  43. package/dist/inspector/vibeables.js +3 -2
  44. package/inspector/dist/assets/dist-B2o7tVdJ.js +23 -0
  45. package/inspector/dist/assets/{dist-Ceimhh7q.js → dist-BA7UaMHM.js} +1 -1
  46. package/inspector/dist/assets/dist-BXQtebmM.js +1 -0
  47. package/inspector/dist/assets/dist-BYDYk38f.js +1 -0
  48. package/inspector/dist/assets/{dist-CPlSMtdY.js → dist-BdZ5cNRp.js} +1 -1
  49. package/inspector/dist/assets/{dist-DGoW6poO.js → dist-BgakrfmB.js} +1 -1
  50. package/inspector/dist/assets/{dist-CprubYd5.js → dist-BpUghI2_.js} +1 -1
  51. package/inspector/dist/assets/{dist-BdimISeT.js → dist-BpYRdsio.js} +1 -1
  52. package/inspector/dist/assets/{dist-C9wdXHwY.js → dist-CHSqB1rM.js} +1 -1
  53. package/inspector/dist/assets/{dist-B1xH7Mxz.js → dist-CVpPKkOb.js} +1 -1
  54. package/inspector/dist/assets/{dist-H3rerbMg.js → dist-CYzOYk_e.js} +9 -9
  55. package/inspector/dist/assets/dist-CzkTIkYD.js +1 -0
  56. package/inspector/dist/assets/{dist-rE2oRtJp.js → dist-D7O3CloR.js} +1 -1
  57. package/inspector/dist/assets/dist-DJzkq2t-.js +1 -0
  58. package/inspector/dist/assets/dist-Dctj7Mr3.js +1 -0
  59. package/inspector/dist/assets/{dist-BA9oX2Qo.js → dist-DtEBn0Pj.js} +1 -1
  60. package/inspector/dist/assets/{dist-CmUexHfa.js → dist-DwpQHmP7.js} +1 -1
  61. package/inspector/dist/assets/{dist-CF7An11X.js → dist-KG4lrraG.js} +1 -1
  62. package/inspector/dist/assets/{dist-5wk4daHl.js → dist-Ko3fuaif.js} +1 -1
  63. package/inspector/dist/assets/{dist-ZMc9InB9.js → dist-f7v359XP.js} +1 -1
  64. package/inspector/dist/assets/{dist-yFKhRrHN.js → dist-itDgDZqI.js} +1 -1
  65. package/inspector/dist/assets/{dist-CXS6W3P9.js → dist-p_qW90fR.js} +1 -1
  66. package/inspector/dist/assets/{dist-DNBDudvB.js → dist-xi1nJU9x.js} +1 -1
  67. package/inspector/dist/assets/editor-BkGiMzB_.js +120 -0
  68. package/inspector/dist/assets/editor-CZMrhTTL.css +1 -0
  69. package/inspector/dist/assets/index-Bb27CwDb.css +2 -0
  70. package/inspector/dist/assets/index-D4h-VE-4.js +229 -0
  71. package/inspector/dist/assets/material-symbols-rounded-Dni8jOUD.woff2 +0 -0
  72. package/inspector/dist/index.html +2 -2
  73. package/package.json +1 -1
  74. package/inspector/dist/assets/dist-9zOw_mLO.js +0 -1
  75. package/inspector/dist/assets/dist-D3a-4Le2.js +0 -1
  76. package/inspector/dist/assets/dist-D8nmL5S9.js +0 -1
  77. package/inspector/dist/assets/dist-DN5yqrtf.js +0 -1
  78. package/inspector/dist/assets/dist-DaG9QYNU.js +0 -1
  79. package/inspector/dist/assets/dist-_9tJHb24.js +0 -23
  80. package/inspector/dist/assets/editor-B1Jq9YIq.css +0 -1
  81. package/inspector/dist/assets/editor-DtVtHids.js +0 -117
  82. package/inspector/dist/assets/index-B-sri0As.js +0 -225
  83. package/inspector/dist/assets/index-Da4dQf-y.css +0 -1
  84. package/inspector/dist/assets/material-symbols-rounded-DzDTtRam.woff2 +0 -0
@@ -2,6 +2,23 @@ import type { Attachment, ChatAgentId, ChatMeta, ChatScope, ElicitationAnswer, S
2
2
  import { type Channel } from "../channels.js";
3
3
  /** Server shutdown: no agent subprocess may outlive the inspector. */
4
4
  export declare function disposeAllBackends(): void;
5
+ /**
6
+ * "## Your journal" block: the recent part of agents/<slug>/journal.md and
7
+ * how to add to it. This is what makes the agent the same colleague from
8
+ * one session to the next.
9
+ */
10
+ export declare function agentJournalContext(def: {
11
+ slug: string;
12
+ harness: ChatAgentId;
13
+ }): Promise<string>;
14
+ /**
15
+ * "## Files" block for an agent: the workspace's files and the folders in
16
+ * it linked to the agent. An agent owns no files; in a project it works
17
+ * with the project's (the project block says where).
18
+ */
19
+ export declare function agentFilesContext(def: {
20
+ files?: string[];
21
+ }): Promise<string>;
5
22
  /**
6
23
  * "## Your vibeables" block: the apps an agent builds and maintains, linked
7
24
  * in agent.yml like knowledge. Empty when nothing is linked. When the
@@ -36,6 +53,34 @@ export declare function createChat(opts: {
36
53
  }): Promise<ChatMeta>;
37
54
  /** A permission request is waiting for a human in this chat (live state only — a restart drops it with the backend). */
38
55
  export declare function chatAwaitingApproval(id: string): boolean;
56
+ /** A request in a chat that waits for a human: an approval or a question, with the seat (agent) that raised it. */
57
+ export interface WaitingRequest {
58
+ requestId: string;
59
+ kind: "approval" | "question";
60
+ title: string;
61
+ since: string;
62
+ meta: ChatMeta;
63
+ /** The seat that asked: MAIN in an ordinary chat, the agent slug in a channel. */
64
+ seat: string;
65
+ }
66
+ /** Every request waiting for a human, across the chats in memory. */
67
+ export declare function waitingRequests(): WaitingRequest[];
68
+ /** One chat an agent is in right now: mid-turn, or waiting on a human. */
69
+ export interface AgentActivity {
70
+ chatId: string;
71
+ title: string;
72
+ since?: string;
73
+ }
74
+ /**
75
+ * Live work per agent slug, from the chats in memory: where its seat is
76
+ * mid-turn (working) and where it raised a permission request or a
77
+ * question nobody answered yet (waiting). Agent sessions and channel seats
78
+ * alike; plain and project chats have no agent and are left out.
79
+ */
80
+ export declare function agentActivity(): Map<string, {
81
+ working: AgentActivity[];
82
+ waiting: AgentActivity[];
83
+ }>;
39
84
  export declare function listChats(): Promise<Array<ChatMeta & {
40
85
  busy: boolean;
41
86
  awaitingApproval: boolean;
@@ -132,10 +177,16 @@ export declare function listAgentSessionsFor(agent: ChatAgentId): Promise<Array<
132
177
  export declare function resolvePermission(id: string, requestId: string, optionId: string | null): Promise<{
133
178
  error?: string;
134
179
  }>;
135
- /** Delete a chat: kill its backend, drop the live state, remove its folder. */
180
+ /**
181
+ * Delete a chat: kill its backend, drop the live state, move its folder to
182
+ * the trash. The agents' own transcripts stay until it is deleted from the
183
+ * trash (purgeChatTranscripts), so a restored chat can resume.
184
+ */
136
185
  export declare function deleteChat(id: string): Promise<{
137
186
  error?: string;
138
187
  }>;
188
+ /** The agents' own transcripts of a chat folder (one in the trash), deleted for good. Best effort. */
189
+ export declare function purgeChatTranscripts(dir: string): Promise<void>;
139
190
  /**
140
191
  * Interrupt the agent's turn — in a channel, one agent's (`agent` = its
141
192
  * slug) or every agent's. Whatever the agent was waiting on from a human
@@ -14,7 +14,10 @@ import { startPiBackend } from "./pi.js";
14
14
  import { UNATTENDED_PERMISSION_TIMEOUT_MS, declineOption, unattendedTimeoutLabel } from "./permissions.js";
15
15
  import { chatHref, dismissKey, pushNotification } from "../notifications.js";
16
16
  import { getChannel, mentionTargets } from "../channels.js";
17
- import { appendEvent, attachmentPath, copyChatFiles, listChatMetas, newChatId, readEvents, readMeta, safeChatDir, writeMeta, } from "./store.js";
17
+ import { moveToTrash } from "../trash.js";
18
+ import { JOURNAL_FILE, journalExcerpt, readJournal } from "../journal.js";
19
+ import { filesSummary } from "../files.js";
20
+ import { appendEvent, attachmentPath, copyChatFiles, listChatMetas, newChatId, readEvents, readMeta, readMetaAt, safeChatDir, writeMeta, } from "./store.js";
18
21
  const MAIN = "main";
19
22
  const states = new Map();
20
23
  const newSeat = (key, harness) => ({
@@ -55,6 +58,28 @@ function chatLabel(meta) {
55
58
  return meta.title;
56
59
  return meta.scope.kind === "agent" ? meta.scope.slug : "chat";
57
60
  }
61
+ /**
62
+ * Whose a chat is, owner first, for a notification: "📁 Relaunch · 🦊 Lisa",
63
+ * "🦊 Lisa", "#launch · 🐻 Max", "Ralv · <chat title>". The project or
64
+ * agent comes before the chat because that is where you go.
65
+ */
66
+ async function ownerLabel(meta, seatKey) {
67
+ const agentName = async (slug) => {
68
+ const a = await getAgent(slug).catch(() => null);
69
+ return a ? `${a.emoji ? `${a.emoji} ` : ""}${a.name}` : slug;
70
+ };
71
+ const projectName = async (slug) => `📁 ${(await getProject(slug).catch(() => null))?.title ?? slug}`;
72
+ const { scope } = meta;
73
+ if (scope.kind === "agent")
74
+ return agentName(scope.slug);
75
+ if (scope.kind === "project")
76
+ return projectName(scope.slug);
77
+ if (scope.kind === "channel") {
78
+ const parts = [meta.project ? await projectName(meta.project) : `#${scope.slug}`, await agentName(seatKey)];
79
+ return parts.join(" · ");
80
+ }
81
+ return `Ralv${meta.title ? ` · ${meta.title}` : ""}`;
82
+ }
58
83
  /** Text of the agent's final message (or last error) after `afterSeq`, for a notification body. */
59
84
  function lastEventText(state, afterSeq, type) {
60
85
  const parts = [];
@@ -133,6 +158,7 @@ async function loadState(id) {
133
158
  pendingPermissions: new Map(),
134
159
  pendingElicitations: new Map(),
135
160
  pendingSeat: new Map(),
161
+ pendingInfo: new Map(),
136
162
  writeChain: Promise.resolve(),
137
163
  };
138
164
  // Two racing loads: keep whichever registered first.
@@ -290,6 +316,50 @@ async function agentKnowledgeContext(def) {
290
316
  `Never hand-edit index.md or log.md, and never add \`verified\` yourself — verification is the human's ` +
291
317
  `click in the UI.\n\n`);
292
318
  }
319
+ /**
320
+ * "## Your journal" block: the recent part of agents/<slug>/journal.md and
321
+ * how to add to it. This is what makes the agent the same colleague from
322
+ * one session to the next.
323
+ */
324
+ export async function agentJournalContext(def) {
325
+ const { text, truncated } = journalExcerpt(await readJournal(def.slug).catch(() => ""));
326
+ return (`## Your journal
327
+ Your own notes from earlier sessions, newest first (agents/${def.slug}/${JOURNAL_FILE}). ` +
328
+ `They are your memory between sessions: build on them, keep what you promised, and check facts that may have changed since.
329
+
330
+ ` +
331
+ (text ? `${text}
332
+ ` : `(empty: nothing recorded yet)
333
+ `) +
334
+ (truncated ? `(older days are in the file; read it when you need them)
335
+ ` : "") +
336
+ `
337
+ Add to it when something should outlive this session, one line each:
338
+ ` +
339
+ ` npx kraftwerk journal ${def.slug} "<one line>" --kind learned|decided|promised|done
340
+ ` +
341
+ `learned: a durable fact about the user, the work or the workspace. decided: a decision and its reason. ` +
342
+ `promised: something you said you would do. done: a promise kept or a piece of work finished. ` +
343
+ `Write a few lines at the end of a session that changed something, not a transcript; no secrets. ` +
344
+ `Never edit ${JOURNAL_FILE} by hand.
345
+
346
+ `);
347
+ }
348
+ /**
349
+ * "## Files" block for an agent: the workspace's files and the folders in
350
+ * it linked to the agent. An agent owns no files; in a project it works
351
+ * with the project's (the project block says where).
352
+ */
353
+ export async function agentFilesContext(def) {
354
+ const ws = await filesSummary("workspace").catch(() => null);
355
+ if (!ws)
356
+ return "";
357
+ const linked = (def.files ?? []).map((f) => `- ${ws.root}/${f}/`).join("\n");
358
+ return (`## Files\nThe workspace's shared files (any format: brand assets, templates, contracts, …) are in ${ws.root}/ (${ws.count} file${ws.count === 1 ? "" : "s"}). ` +
359
+ (linked ? `The folders that are part of your job:\n${linked}\n` : "") +
360
+ `Work you do for a project belongs in that project's files folder, not here. What you produce in a direct session goes to ${ws.root}/inbox/. ` +
361
+ `Delete nothing there unless asked.\n\n`);
362
+ }
293
363
  /**
294
364
  * "## Your vibeables" block: the apps an agent builds and maintains, linked
295
365
  * in agent.yml like knowledge. Empty when nothing is linked. When the
@@ -425,6 +495,8 @@ async function baseScopeContext(scope, agent) {
425
495
  : "") +
426
496
  (await agentKnowledgeContext(def)) +
427
497
  (await agentVibeablesContext(def)) +
498
+ (await agentJournalContext(def)) +
499
+ (await agentFilesContext(def)) +
428
500
  `The working directory is the project root. Stay within your role; if a request is clearly outside it, ` +
429
501
  `say so and suggest which agent or tool fits better.` +
430
502
  (scope.routine
@@ -508,6 +580,7 @@ async function baseScopeContext(scope, agent) {
508
580
  `## Agents\n${agentLines || "(none)"}\n` +
509
581
  `These are persistent agents (defined under agents/); the user talks to them on the ` +
510
582
  `Agents screen. Point the user there when a request clearly belongs to one of them.\n\n` +
583
+ (await agentFilesContext({})) +
511
584
  `Answer questions about workflows and runs by reading the files above. Do not modify run outputs unless asked.`);
512
585
  }
513
586
  if (scope.kind === "run") {
@@ -645,23 +718,32 @@ async function ensureBackend(state, seat) {
645
718
  const notifyKey = `approval:${state.meta.id}:${requestId}`;
646
719
  let deadline;
647
720
  state.pendingSeat.set(requestId, seat.key);
721
+ state.pendingInfo.set(requestId, { kind: "approval", title, since: new Date().toISOString() });
648
722
  state.pendingPermissions.set(requestId, (optionId) => {
649
723
  if (deadline)
650
724
  clearTimeout(deadline);
651
725
  state.pendingPermissions.delete(requestId);
652
726
  state.pendingSeat.delete(requestId);
727
+ state.pendingInfo.delete(requestId);
653
728
  emit(state, { type: "permission_resolved", requestId, optionId }, me);
654
729
  void dismissKey(notifyKey);
655
730
  resolve(optionId);
656
731
  });
657
732
  emit(state, { type: "permission_request", requestId, title, options }, me);
658
733
  // The bell: a question is waiting. Answered -> the item goes away again.
659
- void pushNotification({
660
- kind: "approval",
661
- key: notifyKey,
662
- title: `${chatLabel(state.meta)}${me?.kind === "agent" ? ` · @${me.slug}` : ""} needs approval`,
663
- body: title,
664
- href: chatHref(state.meta),
734
+ // The owner lookup is async: answered meanwhile, there is nothing to notify about (and a late item is taken back).
735
+ void ownerLabel(state.meta, seat.key).then(async (who) => {
736
+ if (!state.pendingInfo.has(requestId))
737
+ return;
738
+ await pushNotification({
739
+ kind: "approval",
740
+ key: notifyKey,
741
+ title: `${who} needs approval`,
742
+ body: title,
743
+ href: `${chatHref(state.meta)}?focus=${encodeURIComponent(requestId)}`,
744
+ });
745
+ if (!state.pendingInfo.has(requestId))
746
+ await dismissKey(notifyKey);
665
747
  });
666
748
  if (isUnattended(state.meta.scope)) {
667
749
  deadline = setTimeout(() => {
@@ -688,22 +770,31 @@ async function ensureBackend(state, seat) {
688
770
  const notifyKey = `question:${state.meta.id}:${requestId}`;
689
771
  let deadline;
690
772
  state.pendingSeat.set(requestId, seat.key);
773
+ state.pendingInfo.set(requestId, { kind: "question", title: message, since: new Date().toISOString() });
691
774
  state.pendingElicitations.set(requestId, (answer) => {
692
775
  if (deadline)
693
776
  clearTimeout(deadline);
694
777
  state.pendingElicitations.delete(requestId);
695
778
  state.pendingSeat.delete(requestId);
779
+ state.pendingInfo.delete(requestId);
696
780
  emit(state, { type: "elicitation_resolved", requestId, ...answer }, me);
697
781
  void dismissKey(notifyKey);
698
782
  resolve(answer);
699
783
  });
700
784
  emit(state, { type: "elicitation_request", requestId, message, fields }, me);
701
- void pushNotification({
702
- kind: "approval",
703
- key: notifyKey,
704
- title: `${chatLabel(state.meta)}${me?.kind === "agent" ? ` · @${me.slug}` : ""} has a question`,
705
- body: message,
706
- href: chatHref(state.meta),
785
+ // The owner lookup is async: answered meanwhile, there is nothing to notify about (and a late item is taken back).
786
+ void ownerLabel(state.meta, seat.key).then(async (who) => {
787
+ if (!state.pendingInfo.has(requestId))
788
+ return;
789
+ await pushNotification({
790
+ kind: "approval",
791
+ key: notifyKey,
792
+ title: `${who} has a question`,
793
+ body: message,
794
+ href: `${chatHref(state.meta)}?focus=${encodeURIComponent(requestId)}`,
795
+ });
796
+ if (!state.pendingInfo.has(requestId))
797
+ await dismissKey(notifyKey);
707
798
  });
708
799
  if (isUnattended(state.meta.scope)) {
709
800
  deadline = setTimeout(() => {
@@ -775,13 +866,48 @@ export async function createChat(opts) {
775
866
  pendingPermissions: new Map(),
776
867
  pendingElicitations: new Map(),
777
868
  pendingSeat: new Map(),
869
+ pendingInfo: new Map(),
778
870
  writeChain: Promise.resolve(),
779
871
  });
780
872
  return meta;
781
873
  }
782
874
  /** A permission request is waiting for a human in this chat (live state only — a restart drops it with the backend). */
783
875
  export function chatAwaitingApproval(id) {
784
- return (states.get(id)?.pendingPermissions.size ?? 0) > 0;
876
+ return (states.get(id)?.pendingInfo.size ?? 0) > 0;
877
+ }
878
+ /** Every request waiting for a human, across the chats in memory. */
879
+ export function waitingRequests() {
880
+ const out = [];
881
+ for (const state of states.values()) {
882
+ for (const [requestId, info] of state.pendingInfo) {
883
+ out.push({ requestId, ...info, meta: state.meta, seat: state.pendingSeat.get(requestId) ?? MAIN });
884
+ }
885
+ }
886
+ return out;
887
+ }
888
+ /**
889
+ * Live work per agent slug, from the chats in memory: where its seat is
890
+ * mid-turn (working) and where it raised a permission request or a
891
+ * question nobody answered yet (waiting). Agent sessions and channel seats
892
+ * alike; plain and project chats have no agent and are left out.
893
+ */
894
+ export function agentActivity() {
895
+ const out = new Map();
896
+ const of = (slug) => out.get(slug) ?? out.set(slug, { working: [], waiting: [] }).get(slug);
897
+ for (const state of states.values()) {
898
+ const { meta } = state;
899
+ for (const seat of state.seats.values()) {
900
+ const slug = meta.scope.kind === "agent" ? meta.scope.slug : meta.scope.kind === "channel" ? seat.key : undefined;
901
+ if (!slug)
902
+ continue;
903
+ const item = { chatId: meta.id, title: meta.title };
904
+ if (seat.busy)
905
+ of(slug).working.push({ ...item, ...(seat.busySince ? { since: seat.busySince } : {}) });
906
+ if ([...state.pendingSeat.values()].includes(seat.key))
907
+ of(slug).waiting.push(item);
908
+ }
909
+ }
910
+ return out;
785
911
  }
786
912
  export async function listChats() {
787
913
  const metas = await listChatMetas();
@@ -810,6 +936,7 @@ export async function postMessage(id, text, opts = {}) {
810
936
  if (seat.busy)
811
937
  return { error: "agent is still working — wait for the turn to finish" };
812
938
  seat.busy = true;
939
+ seat.busySince = new Date().toISOString();
813
940
  const isFirst = !state.events.some((e) => e.type === "user_message");
814
941
  if (isFirst && !state.meta.title) {
815
942
  state.meta.title = text.replace(/\s+/g, " ").trim().slice(0, 80);
@@ -841,7 +968,7 @@ export async function postMessage(id, text, opts = {}) {
841
968
  const failed = state.events.some((e) => e.seq > turnStart && (e.type === "error" || (e.type === "failure" && e.severity === "error")));
842
969
  void pushNotification({
843
970
  kind: failed ? "routine_failed" : "routine_done",
844
- title: `${chatLabel(state.meta)} ${failed ? "ended with an error" : "finished"}`,
971
+ title: `${await ownerLabel(state.meta, MAIN)} · ${chatLabel(state.meta)} ${failed ? "ended with an error" : "finished"}`,
845
972
  body: failed ? lastEventText(state, turnStart, "error") : lastEventText(state, turnStart, "text"),
846
973
  href: chatHref(state.meta),
847
974
  ...(failed ? { diagnose: routineRef(state.meta) } : {}),
@@ -853,7 +980,7 @@ export async function postMessage(id, text, opts = {}) {
853
980
  if (isUnattended(state.meta.scope)) {
854
981
  void pushNotification({
855
982
  kind: "routine_failed",
856
- title: `${chatLabel(state.meta)} failed`,
983
+ title: `${await ownerLabel(state.meta, MAIN)} · ${chatLabel(state.meta)} failed`,
857
984
  body: err.message,
858
985
  href: chatHref(state.meta),
859
986
  diagnose: routineRef(state.meta),
@@ -950,6 +1077,7 @@ function seatTextSince(state, afterSeq, slug) {
950
1077
  }
951
1078
  async function runSeatTurn(state, channel, seat, hops) {
952
1079
  seat.busy = true;
1080
+ seat.busySince = new Date().toISOString();
953
1081
  const me = { kind: "agent", slug: seat.key };
954
1082
  const start = emit(state, { type: "turn_start" }, me).seq;
955
1083
  try {
@@ -1270,7 +1398,11 @@ export async function resolvePermission(id, requestId, optionId) {
1270
1398
  resolve(optionId);
1271
1399
  return {};
1272
1400
  }
1273
- /** Delete a chat: kill its backend, drop the live state, remove its folder. */
1401
+ /**
1402
+ * Delete a chat: kill its backend, drop the live state, move its folder to
1403
+ * the trash. The agents' own transcripts stay until it is deleted from the
1404
+ * trash (purgeChatTranscripts), so a restored chat can resume.
1405
+ */
1274
1406
  export async function deleteChat(id) {
1275
1407
  const state = await loadState(id);
1276
1408
  if (!state)
@@ -1281,18 +1413,19 @@ export async function deleteChat(id) {
1281
1413
  resolve({ action: "cancel" });
1282
1414
  dropBackend(state);
1283
1415
  states.delete(id);
1284
- await fs.rm(safeChatDir(id), { recursive: true, force: true });
1285
- // The agent's own transcripts go with the chat (best effort, in the background).
1286
- const cwd = await effectiveCwd(state.meta);
1287
- for (const [seatKey, sessionId] of Object.entries(state.meta.sessions ?? {})) {
1288
- void (async () => {
1289
- const harness = seatKey === MAIN ? state.meta.agent : (await getAgent(seatKey).catch(() => null))?.harness;
1290
- if (harness && harness !== "pi")
1291
- await deleteAgentSession(harness, cwd, sessionId);
1292
- })().catch(() => { });
1293
- }
1416
+ await moveToTrash("chats", id, safeChatDir(id));
1294
1417
  return {};
1295
1418
  }
1419
+ /** The agents' own transcripts of a chat folder (one in the trash), deleted for good. Best effort. */
1420
+ export async function purgeChatTranscripts(dir) {
1421
+ const meta = await readMetaAt(dir);
1422
+ const cwd = await effectiveCwd(meta);
1423
+ for (const [seatKey, sessionId] of Object.entries(meta.sessions ?? {})) {
1424
+ const harness = seatKey === MAIN ? meta.agent : (await getAgent(seatKey).catch(() => null))?.harness;
1425
+ if (harness && harness !== "pi")
1426
+ await deleteAgentSession(harness, cwd, sessionId).catch(() => { });
1427
+ }
1428
+ }
1296
1429
  /**
1297
1430
  * Interrupt the agent's turn — in a channel, one agent's (`agent` = its
1298
1431
  * slug) or every agent's. Whatever the agent was waiting on from a human
@@ -3,6 +3,8 @@ export declare function safeChatDir(id: string): string;
3
3
  export declare function newChatId(): string;
4
4
  export declare function writeMeta(meta: ChatMeta): Promise<void>;
5
5
  export declare function readMeta(id: string): Promise<ChatMeta | null>;
6
+ /** meta.json of a chat folder wherever it is (also one in the trash); throws when unreadable. */
7
+ export declare function readMetaAt(dir: string): Promise<ChatMeta>;
6
8
  /** Where a chat keeps the files dropped into it. */
7
9
  export declare function attachmentsDir(id: string): string;
8
10
  /** A stored attachment's path; rejects names that could leave the folder. */
@@ -26,12 +26,16 @@ export async function writeMeta(meta) {
26
26
  }
27
27
  export async function readMeta(id) {
28
28
  try {
29
- return upgradeMeta(JSON.parse(await fs.readFile(path.join(safeChatDir(id), "meta.json"), "utf8")));
29
+ return await readMetaAt(safeChatDir(id));
30
30
  }
31
31
  catch {
32
32
  return null;
33
33
  }
34
34
  }
35
+ /** meta.json of a chat folder wherever it is (also one in the trash); throws when unreadable. */
36
+ export async function readMetaAt(dir) {
37
+ return upgradeMeta(JSON.parse(await fs.readFile(path.join(dir, "meta.json"), "utf8")));
38
+ }
35
39
  /**
36
40
  * Chats written before 0.36 scoped agent sessions as
37
41
  * { kind: "team", member } — read them as { kind: "agent", slug } so the
@@ -0,0 +1,87 @@
1
+ import type { Readable } from "node:stream";
2
+ /**
3
+ * Files: the material of the work, any format, as plain folders. Two
4
+ * owners, by how long a file lives:
5
+ *
6
+ * kraftwerk-data/files/ workspace files: shared, outlive any project
7
+ * kraftwerk-data/projects/<slug>/files/ a project's files: briefing, drafts, deliverables
8
+ *
9
+ * Agents own none (they link folders; see agentFilesContext). Being plain
10
+ * folders, agents read and write them like any directory, the workspace
11
+ * git keeps their history, and a delete goes to the trash. Files over
12
+ * LOCAL_OVER_BYTES stay out of git: listed in the root's own .gitignore,
13
+ * shown as "local only". Knowledge is separate: a file becomes evidence in
14
+ * a bundle's references/ through "learn from this" (later).
15
+ */
16
+ export declare const WORKSPACE_FILES_DIR = "kraftwerk-data/files";
17
+ export declare const PROJECT_FILES_DIR = "files";
18
+ /** Bigger files stay local: kept out of the workspace git. */
19
+ export declare const LOCAL_OVER_BYTES: number;
20
+ /** Upload cap: a guard against a runaway request, not a policy. */
21
+ export declare const MAX_UPLOAD_BYTES: number;
22
+ /** "workspace" or "project:<slug>". */
23
+ export type FilesScope = string;
24
+ export interface FileEntry {
25
+ name: string;
26
+ /** Path inside the scope's root, forward slashes. */
27
+ path: string;
28
+ type: "dir" | "file";
29
+ size?: number;
30
+ modifiedAt: string;
31
+ /** dirs: files below it (recursive, capped). */
32
+ count?: number;
33
+ mime?: string;
34
+ /** Over the size limit: kept out of git. */
35
+ local?: boolean;
36
+ }
37
+ export interface FilesListing {
38
+ scope: FilesScope;
39
+ label: string;
40
+ /** The folder, inside the root ("" = the root). */
41
+ path: string;
42
+ /** The root relative to the workspace, for the agent and the UI. */
43
+ root: string;
44
+ entries: FileEntry[];
45
+ }
46
+ export interface FilesScopeInfo {
47
+ scope: FilesScope;
48
+ label: string;
49
+ root: string;
50
+ count: number;
51
+ status?: string;
52
+ }
53
+ export declare const mimeOf: (name: string) => string;
54
+ /** The root folder of a scope, its display label, and the root relative to the workspace. */
55
+ export declare function scopeRoot(scope: FilesScope): Promise<{
56
+ abs: string;
57
+ rel: string;
58
+ label: string;
59
+ workspace: string;
60
+ }>;
61
+ export declare function listScopes(): Promise<FilesScopeInfo[]>;
62
+ export declare function listFiles(scope: FilesScope, rel?: string): Promise<FilesListing>;
63
+ /** A file to stream to the browser. */
64
+ export declare function openFile(scope: FilesScope, rel: string): Promise<{
65
+ stream: Readable;
66
+ size: number;
67
+ mime: string;
68
+ name: string;
69
+ }>;
70
+ /** Stream an upload into a folder; a name already taken gets " (2)". */
71
+ export declare function saveUpload(scope: FilesScope, dirRel: string, name: string, body: Readable): Promise<FileEntry>;
72
+ export declare function makeFolder(scope: FilesScope, rel: string): Promise<FileEntry>;
73
+ /** Rename or move inside the scope. Refused when the target exists. */
74
+ export declare function moveFile(scope: FilesScope, fromRel: string, toRel: string): Promise<void>;
75
+ /** Move a file or folder to the trash (trash/files/<workspace | projects/slug>/<path>). */
76
+ export declare function deleteFile(scope: FilesScope, rel: string): Promise<void>;
77
+ /** "files/ (23 files; recent: designs/home-v3.png 2026-10-05, …)" — where the files are, not every file. */
78
+ export declare function filesSummary(scope: FilesScope, recent?: number): Promise<{
79
+ root: string;
80
+ count: number;
81
+ recent: {
82
+ path: string;
83
+ at: string;
84
+ }[];
85
+ } | null>;
86
+ /** The "## Files" block of a session's context: the folder to use, what is in it lately. */
87
+ export declare function filesContext(scope: FilesScope, heading: string, intro: string): Promise<string>;