wave-agent-sdk 0.14.1 → 0.14.2

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 (78) hide show
  1. package/builtin/skills/settings/HOOKS.md +69 -0
  2. package/builtin/skills/settings/PLUGINS.md +171 -0
  3. package/builtin/skills/settings/SKILL.md +8 -3
  4. package/dist/agent.d.ts +2 -2
  5. package/dist/agent.d.ts.map +1 -1
  6. package/dist/agent.js +12 -3
  7. package/dist/managers/aiManager.d.ts +6 -6
  8. package/dist/managers/aiManager.d.ts.map +1 -1
  9. package/dist/managers/aiManager.js +122 -59
  10. package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
  11. package/dist/managers/backgroundTaskManager.js +28 -18
  12. package/dist/managers/hookManager.d.ts +16 -1
  13. package/dist/managers/hookManager.d.ts.map +1 -1
  14. package/dist/managers/hookManager.js +97 -8
  15. package/dist/managers/messageManager.d.ts +19 -4
  16. package/dist/managers/messageManager.d.ts.map +1 -1
  17. package/dist/managers/messageManager.js +63 -18
  18. package/dist/prompts/index.d.ts +1 -1
  19. package/dist/prompts/index.d.ts.map +1 -1
  20. package/dist/prompts/index.js +1 -1
  21. package/dist/services/MarketplaceService.d.ts +0 -11
  22. package/dist/services/MarketplaceService.d.ts.map +1 -1
  23. package/dist/services/MarketplaceService.js +21 -89
  24. package/dist/services/aiService.d.ts +3 -3
  25. package/dist/services/aiService.d.ts.map +1 -1
  26. package/dist/services/aiService.js +7 -7
  27. package/dist/services/hook.d.ts.map +1 -1
  28. package/dist/services/hook.js +15 -0
  29. package/dist/services/initializationService.d.ts.map +1 -1
  30. package/dist/services/initializationService.js +24 -1
  31. package/dist/services/interactionService.js +1 -1
  32. package/dist/services/pluginLoader.d.ts +0 -6
  33. package/dist/services/pluginLoader.d.ts.map +1 -1
  34. package/dist/services/pluginLoader.js +14 -53
  35. package/dist/services/session.d.ts +1 -1
  36. package/dist/services/session.js +7 -7
  37. package/dist/services/taskManager.d.ts +1 -1
  38. package/dist/services/taskManager.js +1 -1
  39. package/dist/types/core.d.ts +1 -1
  40. package/dist/types/core.d.ts.map +1 -1
  41. package/dist/types/hooks.d.ts +9 -1
  42. package/dist/types/hooks.d.ts.map +1 -1
  43. package/dist/types/hooks.js +2 -0
  44. package/dist/types/marketplace.d.ts +1 -26
  45. package/dist/types/marketplace.d.ts.map +1 -1
  46. package/dist/types/messaging.d.ts +3 -3
  47. package/dist/types/messaging.d.ts.map +1 -1
  48. package/dist/types/plugins.d.ts +1 -13
  49. package/dist/types/plugins.d.ts.map +1 -1
  50. package/dist/utils/convertMessagesForAPI.d.ts +1 -1
  51. package/dist/utils/convertMessagesForAPI.js +7 -7
  52. package/dist/utils/groupMessagesByApiRound.d.ts +1 -1
  53. package/dist/utils/groupMessagesByApiRound.js +6 -6
  54. package/dist/utils/messageOperations.d.ts.map +1 -1
  55. package/dist/utils/messageOperations.js +3 -3
  56. package/package.json +1 -1
  57. package/src/agent.ts +16 -3
  58. package/src/managers/aiManager.ts +142 -63
  59. package/src/managers/backgroundTaskManager.ts +32 -22
  60. package/src/managers/hookManager.ts +125 -10
  61. package/src/managers/messageManager.ts +76 -22
  62. package/src/prompts/index.ts +1 -1
  63. package/src/services/MarketplaceService.ts +26 -127
  64. package/src/services/aiService.ts +11 -11
  65. package/src/services/hook.ts +17 -0
  66. package/src/services/initializationService.ts +33 -1
  67. package/src/services/interactionService.ts +1 -1
  68. package/src/services/pluginLoader.ts +14 -67
  69. package/src/services/session.ts +7 -7
  70. package/src/services/taskManager.ts +1 -1
  71. package/src/types/core.ts +1 -1
  72. package/src/types/hooks.ts +16 -2
  73. package/src/types/marketplace.ts +1 -24
  74. package/src/types/messaging.ts +3 -3
  75. package/src/types/plugins.ts +1 -13
  76. package/src/utils/convertMessagesForAPI.ts +8 -8
  77. package/src/utils/groupMessagesByApiRound.ts +6 -6
  78. package/src/utils/messageOperations.ts +3 -5
@@ -47,8 +47,8 @@ export interface MessageManagerCallbacks {
47
47
  onAssistantReasoningUpdated?: (chunk: string, accumulated: string) => void;
48
48
  onToolBlockUpdated?: (params: AgentToolBlockUpdateParams) => void;
49
49
  onErrorBlockAdded?: (error: string) => void;
50
- onCompressBlockAdded?: (content: string) => void;
51
- onCompressionStateChange?: (isCompressing: boolean) => void;
50
+ onCompactBlockAdded?: (content: string) => void;
51
+ onCompactionStateChange?: (isCompacting: boolean) => void;
52
52
  // Bang callback
53
53
  onAddBangMessage?: (command: string) => void;
54
54
  onUpdateBangMessage?: (command: string, output: string) => void;
@@ -92,6 +92,8 @@ export class MessageManager {
92
92
  private filesInContext: Set<string> = new Set(); // Track files mentioned in the conversation
93
93
  private recentFileReads: Map<string, { content: string; timestamp: number }> =
94
94
  new Map(); // Track file read contents
95
+ private invokedSkills: Map<string, { skillName: string; timestamp: number }> =
96
+ new Map(); // Track invoked skill names
95
97
  private sessionType: "main" | "subagent";
96
98
  private subagentType?: string;
97
99
  private _usages: Usage[] = [];
@@ -270,12 +272,14 @@ export class MessageManager {
270
272
  for (const message of newMessages) {
271
273
  this.addPathsFromMessage(message);
272
274
  this.extractFileReadsFromMessage(message);
275
+ this.extractSkillInvocationsFromMessage(message);
273
276
  }
274
277
 
275
278
  // Also check if the last message was updated (common for tool blocks)
276
279
  if (messages.length > 0 && messages.length === oldLength) {
277
280
  this.addPathsFromMessage(messages[messages.length - 1]);
278
281
  this.extractFileReadsFromMessage(messages[messages.length - 1]);
282
+ this.extractSkillInvocationsFromMessage(messages[messages.length - 1]);
279
283
  }
280
284
 
281
285
  this.callbacks.onMessagesChange?.([...messages]);
@@ -494,31 +498,31 @@ export class MessageManager {
494
498
  }
495
499
 
496
500
  /**
497
- * Compress messages and update session, delete compressed messages, only keep compressed messages and last 3 messages
501
+ * Compact messages and update session, delete compacted messages, only keep compacted messages and last 3 messages
498
502
  */
499
- public compressMessagesAndUpdateSession(
500
- compressedContent: string,
503
+ public compactMessagesAndUpdateSession(
504
+ compactedContent: string,
501
505
  usage?: Usage,
502
506
  ): void {
503
507
  // Get last 2 API rounds to preserve (structurally safe boundary)
504
508
  const lastThreeMessages = getLastApiRounds(this.messages, 2);
505
509
 
506
- // Create compressed message
507
- const compressMessage: Message = {
510
+ // Create compacted message
511
+ const compactMessage: Message = {
508
512
  id: generateMessageId(),
509
513
  role: "assistant",
510
514
  blocks: [
511
515
  {
512
- type: "compress",
513
- content: compressedContent,
516
+ type: "compact",
517
+ content: compactedContent,
514
518
  sessionId: this.sessionId,
515
519
  },
516
520
  ],
517
521
  ...(usage && { usage }),
518
522
  };
519
523
 
520
- // Build new message array: keep the compressed message and last 3 messages
521
- const newMessages: Message[] = [compressMessage, ...lastThreeMessages];
524
+ // Build new message array: keep the compacted message and last 3 messages
525
+ const newMessages: Message[] = [compactMessage, ...lastThreeMessages];
522
526
 
523
527
  // Update sessionId and parentSessionId
524
528
  const oldSessionId = this.sessionId;
@@ -539,15 +543,15 @@ export class MessageManager {
539
543
  this.addPathsFromMessage(message);
540
544
  }
541
545
 
542
- // Scan compressedContent for file mentions
546
+ // Scan compactedContent for file mentions
543
547
  const fileMentionRegex = /(?:^|\s)@([\w.\-/]+)/g;
544
548
  let match;
545
- while ((match = fileMentionRegex.exec(compressedContent)) !== null) {
549
+ while ((match = fileMentionRegex.exec(compactedContent)) !== null) {
546
550
  this.touchFile(match[1]);
547
551
  }
548
552
 
549
- // Trigger compression callback
550
- this.callbacks.onCompressBlockAdded?.(compressedContent);
553
+ // Trigger compaction callback
554
+ this.callbacks.onCompactBlockAdded?.(compactedContent);
551
555
  }
552
556
 
553
557
  public addFileHistoryBlock(
@@ -861,7 +865,7 @@ export class MessageManager {
861
865
  let targetSessionId = this.sessionId;
862
866
  let targetIndexInSession = index;
863
867
 
864
- // We need to be careful here because loadFullMessageThread might have removed "compress" blocks
868
+ // We need to be careful here because loadFullMessageThread might have removed "compact" blocks
865
869
  // Let's re-calculate based on the actual messages returned.
866
870
  // Actually, it's easier to just load sessions one by one again or keep track of counts.
867
871
 
@@ -883,19 +887,19 @@ export class MessageManager {
883
887
  if (!sessionData) continue;
884
888
 
885
889
  const sessionMessages = sessionData.messages;
886
- // If this is not the first session in the thread, it might have a compress block at the start
890
+ // If this is not the first session in the thread, it might have a compact block at the start
887
891
  // that was removed in getFullMessageThread.
888
- const hasCompressBlock = sessionMessages[0]?.blocks.some(
889
- (b) => b.type === "compress",
892
+ const hasCompactBlock = sessionMessages[0]?.blocks.some(
893
+ (b) => b.type === "compact",
890
894
  );
891
895
  const effectiveMessages =
892
- hasCompressBlock && sid !== sessionIds[0]
896
+ hasCompactBlock && sid !== sessionIds[0]
893
897
  ? sessionMessages.slice(1)
894
898
  : sessionMessages;
895
899
 
896
900
  if (remainingIndex < effectiveMessages.length) {
897
901
  targetSessionId = sid;
898
- targetIndexInSession = hasCompressBlock
902
+ targetIndexInSession = hasCompactBlock
899
903
  ? remainingIndex + 1
900
904
  : remainingIndex;
901
905
  break;
@@ -938,7 +942,7 @@ export class MessageManager {
938
942
 
939
943
  // Update in-memory messages to the truncated session messages
940
944
  // We do NOT include ancestor messages here to avoid exceeding context limits.
941
- // The 'compress' block at the start of the session (if any) already summarizes them.
945
+ // The 'compact' block at the start of the session (if any) already summarizes them.
942
946
  this.setMessages(newMessagesInSession);
943
947
 
944
948
  // Update saved message count
@@ -1056,4 +1060,54 @@ export class MessageManager {
1056
1060
  }
1057
1061
  return result;
1058
1062
  }
1063
+
1064
+ /**
1065
+ * Extract skill invocations from tool blocks in a message.
1066
+ */
1067
+ private extractSkillInvocationsFromMessage(message: Message): void {
1068
+ for (const block of message.blocks) {
1069
+ if (
1070
+ block.type === "tool" &&
1071
+ block.name === "Skill" &&
1072
+ block.stage === "end" &&
1073
+ block.parameters
1074
+ ) {
1075
+ try {
1076
+ const params = JSON.parse(block.parameters) as Record<
1077
+ string,
1078
+ unknown
1079
+ >;
1080
+ const skillName = params.skill_name as string | undefined;
1081
+ if (skillName) {
1082
+ this.invokedSkills.set(skillName, {
1083
+ skillName,
1084
+ timestamp: Date.now(),
1085
+ });
1086
+ }
1087
+ } catch {
1088
+ // Ignore parse errors
1089
+ }
1090
+ }
1091
+ }
1092
+ }
1093
+
1094
+ /**
1095
+ * Get recently invoked skill names, sorted by timestamp (newest first).
1096
+ * @param maxSkills - Maximum number of skills to return
1097
+ * @returns Array of skill names sorted by recency
1098
+ */
1099
+ public getInvokedSkillNames(maxSkills = 10): string[] {
1100
+ const sorted = Array.from(this.invokedSkills.entries())
1101
+ .sort(([, a], [, b]) => b.timestamp - a.timestamp)
1102
+ .slice(0, maxSkills);
1103
+
1104
+ return sorted.map(([, { skillName }]) => skillName);
1105
+ }
1106
+
1107
+ /**
1108
+ * Clear all invoked skills (e.g., after compaction).
1109
+ */
1110
+ public clearInvokedSkills(): void {
1111
+ this.invokedSkills.clear();
1112
+ }
1059
1113
  }
@@ -179,7 +179,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
179
179
 
180
180
  export const DEFAULT_SYSTEM_PROMPT = BASE_SYSTEM_PROMPT;
181
181
 
182
- export const COMPRESS_MESSAGES_SYSTEM_PROMPT = `You are continuing work on a software engineering task. Write a detailed continuation summary that will allow you (or another instance of yourself) to resume work efficiently in a future context window where the conversation history will be replaced with this summary.
182
+ export const COMPACT_MESSAGES_SYSTEM_PROMPT = `You are continuing work on a software engineering task. Write a detailed continuation summary that will allow you (or another instance of yourself) to resume work efficiently in a future context window where the conversation history will be replaced with this summary.
183
183
 
184
184
  First, write your analysis in <analysis> tags as a thinking scratchpad:
185
185
  - Chronologically review the conversation
@@ -8,12 +8,12 @@ import {
8
8
  InstalledPlugin,
9
9
  InstalledPluginsRegistry,
10
10
  MarketplaceManifest,
11
- MarketplacePluginEntry,
12
11
  MarketplaceSource,
13
12
  } from "../types/marketplace.js";
14
13
  import { GitService } from "./GitService.js";
15
14
  import { ConfigurationService } from "./configurationService.js";
16
15
  import type { MarketplaceConfig, Scope } from "../types/configuration.js";
16
+ import { logger } from "../utils/globalLogger.js";
17
17
 
18
18
  /**
19
19
  * Marketplace Service
@@ -320,42 +320,19 @@ export class MarketplaceService {
320
320
  await fs.rename(tmpPath, this.installedPluginsPath);
321
321
  }
322
322
 
323
- /**
324
- * Finds the first existing marketplace manifest path.
325
- * Prefers .wave-plugin/ for backward compatibility, falls back to .claude-plugin/.
326
- * Returns null if neither exists.
327
- */
328
- private async findMarketplaceManifestPath(
329
- dir: string,
330
- ): Promise<string | null> {
331
- const waveManifestPath = path.join(dir, ".wave-plugin", "marketplace.json");
332
- const claudeManifestPath = path.join(
333
- dir,
334
- ".claude-plugin",
335
- "marketplace.json",
336
- );
337
-
338
- if (existsSync(waveManifestPath)) {
339
- return waveManifestPath;
340
- }
341
- if (existsSync(claudeManifestPath)) {
342
- return claudeManifestPath;
343
- }
344
- return null;
345
- }
346
-
347
323
  /**
348
324
  * Loads a marketplace manifest from a local path
349
325
  */
350
326
  async loadMarketplaceManifest(
351
327
  marketplacePath: string,
352
328
  ): Promise<MarketplaceManifest> {
353
- const manifestPath =
354
- await this.findMarketplaceManifestPath(marketplacePath);
355
- if (!manifestPath) {
356
- throw new Error(
357
- `Marketplace manifest not found at ${marketplacePath}. Neither .wave-plugin/marketplace.json nor .claude-plugin/marketplace.json exists.`,
358
- );
329
+ const manifestPath = path.join(
330
+ marketplacePath,
331
+ ".wave-plugin",
332
+ "marketplace.json",
333
+ );
334
+ if (!existsSync(manifestPath)) {
335
+ throw new Error(`Marketplace manifest not found at ${manifestPath}`);
359
336
  }
360
337
  const content = await fs.readFile(manifestPath, "utf-8");
361
338
  const manifest = JSON.parse(content);
@@ -665,7 +642,7 @@ export class MarketplaceService {
665
642
  (p) => p.name === plugin.name,
666
643
  );
667
644
  if (!pluginEntry) {
668
- console.warn(
645
+ logger.warn(
669
646
  `Plugin "${plugin.name}" no longer found in marketplace "${marketplace.name}". Uninstalling...`,
670
647
  );
671
648
  try {
@@ -767,74 +744,6 @@ export class MarketplaceService {
767
744
  });
768
745
  }
769
746
 
770
- /**
771
- * Resolves a plugin source into a consistent format for installation.
772
- * Handles both string sources (local paths or git URLs) and object-style MarketplaceSource.
773
- */
774
- private resolvePluginSource(
775
- pluginEntry: MarketplacePluginEntry,
776
- marketplacePath: string,
777
- ): { isGit: boolean; url?: string; ref?: string; localPath: string } {
778
- const { source } = pluginEntry;
779
-
780
- if (typeof source === "string") {
781
- // String source: could be a git URL or a relative local path
782
- const isGitUrl =
783
- source.startsWith("http://") ||
784
- source.startsWith("https://") ||
785
- source.startsWith("git@") ||
786
- source.startsWith("ssh://");
787
-
788
- if (isGitUrl) {
789
- let url = source;
790
- let ref: string | undefined;
791
- if (url.includes("#")) {
792
- [url, ref] = url.split("#");
793
- }
794
- return { isGit: true, url, ref, localPath: "" };
795
- }
796
-
797
- // Relative local path
798
- return { isGit: false, localPath: path.resolve(marketplacePath, source) };
799
- }
800
-
801
- // Object-style source
802
- if (source.source === "git") {
803
- return { isGit: true, url: source.url, ref: source.ref, localPath: "" };
804
- }
805
-
806
- if (source.source === "github") {
807
- return {
808
- isGit: true,
809
- url: `https://github.com/${source.repo}.git`,
810
- ref: source.ref,
811
- localPath: "",
812
- };
813
- }
814
-
815
- if (source.source === "url") {
816
- let url = source.url;
817
- let ref = source.ref;
818
- if (url.includes("#")) {
819
- [url, ref] = url.split("#");
820
- }
821
- return { isGit: true, url, ref, localPath: "" };
822
- }
823
-
824
- if (source.source === "directory") {
825
- return {
826
- isGit: false,
827
- localPath: path.resolve(marketplacePath, source.path),
828
- };
829
- }
830
-
831
- // Exhaustiveness: this should be unreachable given the union type
832
- const _exhaustive: never = source;
833
- throw new Error(
834
- `Unsupported plugin source type: ${(_exhaustive as { source: string }).source}`,
835
- );
836
- }
837
-
838
747
  /**
839
748
  * Installs a plugin from a marketplace
840
749
  */
@@ -863,46 +772,36 @@ export class MarketplaceService {
863
772
  );
864
773
  }
865
774
 
866
- const resolved = this.resolvePluginSource(pluginEntry, marketplacePath);
775
+ const isGitSource =
776
+ pluginEntry.source.startsWith("http://") ||
777
+ pluginEntry.source.startsWith("https://") ||
778
+ pluginEntry.source.startsWith("git@") ||
779
+ pluginEntry.source.startsWith("ssh://");
867
780
 
868
781
  let pluginSrcPath: string;
869
782
  let tempCloneDir: string | undefined;
870
783
 
871
784
  try {
872
- if (resolved.isGit) {
785
+ if (isGitSource) {
873
786
  tempCloneDir = path.join(this.tmpDir, `clone-${Date.now()}`);
874
- await this.gitService.clone(
875
- resolved.url!,
876
- tempCloneDir,
877
- resolved.ref,
878
- );
787
+ let url = pluginEntry.source;
788
+ let ref: string | undefined;
789
+ if (url.includes("#")) {
790
+ [url, ref] = url.split("#");
791
+ }
792
+ await this.gitService.clone(url, tempCloneDir, ref);
879
793
  pluginSrcPath = tempCloneDir;
880
794
  } else {
881
- pluginSrcPath = resolved.localPath;
795
+ pluginSrcPath = path.resolve(marketplacePath, pluginEntry.source);
882
796
  }
883
797
 
884
- let pluginManifestPath: string | undefined;
885
- const wavePluginPath = path.join(
798
+ const pluginManifestPath = path.join(
886
799
  pluginSrcPath,
887
800
  ".wave-plugin",
888
801
  "plugin.json",
889
802
  );
890
- const claudePluginPath = path.join(
891
- pluginSrcPath,
892
- ".claude-plugin",
893
- "plugin.json",
894
- );
895
-
896
- if (existsSync(wavePluginPath)) {
897
- pluginManifestPath = wavePluginPath;
898
- } else if (existsSync(claudePluginPath)) {
899
- pluginManifestPath = claudePluginPath;
900
- }
901
-
902
- if (!pluginManifestPath) {
903
- throw new Error(
904
- `Plugin manifest not found at ${pluginSrcPath}. Neither .wave-plugin/plugin.json nor .claude-plugin/plugin.json exists.`,
905
- );
803
+ if (!existsSync(pluginManifestPath)) {
804
+ throw new Error(`Plugin manifest not found at ${pluginManifestPath}`);
906
805
  }
907
806
 
908
807
  const pluginManifestContent = await fs.readFile(
@@ -917,7 +816,7 @@ export class MarketplaceService {
917
816
  `${pluginName}-${Date.now()}`,
918
817
  );
919
818
  try {
920
- if (resolved.isGit) {
819
+ if (isGitSource) {
921
820
  await fs.rename(pluginSrcPath, tmpPluginDir);
922
821
  tempCloneDir = undefined;
923
822
  } else {
@@ -23,7 +23,7 @@ import * as fs from "fs";
23
23
  import * as path from "path";
24
24
 
25
25
  import {
26
- COMPRESS_MESSAGES_SYSTEM_PROMPT,
26
+ COMPACT_MESSAGES_SYSTEM_PROMPT,
27
27
  WEB_CONTENT_SYSTEM_PROMPT,
28
28
  BTW_SYSTEM_PROMPT,
29
29
  } from "../prompts/index.js";
@@ -751,7 +751,7 @@ async function processStreamingResponse(
751
751
  return result;
752
752
  }
753
753
 
754
- export interface CompressMessagesOptions {
754
+ export interface CompactMessagesOptions {
755
755
  // Resolved configuration
756
756
  gatewayConfig: GatewayConfig;
757
757
  modelConfig: ModelConfig;
@@ -762,7 +762,7 @@ export interface CompressMessagesOptions {
762
762
  model?: string;
763
763
  }
764
764
 
765
- export interface CompressMessagesResult {
765
+ export interface CompactMessagesResult {
766
766
  content: string;
767
767
  usage?: {
768
768
  prompt_tokens: number;
@@ -771,9 +771,9 @@ export interface CompressMessagesResult {
771
771
  };
772
772
  }
773
773
 
774
- export async function compressMessages(
775
- options: CompressMessagesOptions,
776
- ): Promise<CompressMessagesResult> {
774
+ export async function compactMessages(
775
+ options: CompactMessagesOptions,
776
+ ): Promise<CompactMessagesResult> {
777
777
  const { gatewayConfig, modelConfig, messages, abortSignal } = options;
778
778
 
779
779
  // Apply global 1 QPS rate limit
@@ -832,7 +832,7 @@ export async function compressMessages(
832
832
  messages: [
833
833
  {
834
834
  role: "system",
835
- content: COMPRESS_MESSAGES_SYSTEM_PROMPT,
835
+ content: COMPACT_MESSAGES_SYSTEM_PROMPT,
836
836
  },
837
837
  ...cleanedMessages,
838
838
  {
@@ -849,7 +849,7 @@ export async function compressMessages(
849
849
  const content = response.choices[0]?.message?.content?.trim();
850
850
  if (!content) {
851
851
  throw new Error(
852
- "Failed to compress conversation history: Empty response from AI",
852
+ "Failed to compact conversation history: Empty response from AI",
853
853
  );
854
854
  }
855
855
  const usage = response.usage
@@ -866,10 +866,10 @@ export async function compressMessages(
866
866
  };
867
867
  } catch (error) {
868
868
  if ((error as Error).name === "AbortError") {
869
- logger.info("Compression request was aborted");
870
- throw new Error("Compression request was aborted");
869
+ logger.info("Compaction request was aborted");
870
+ throw new Error("Compaction request was aborted");
871
871
  }
872
- logger.error("Failed to compress messages:", error);
872
+ logger.error("Failed to compact messages:", error);
873
873
  throw error;
874
874
  }
875
875
  }
@@ -84,6 +84,23 @@ async function buildHookJsonInput(
84
84
  }
85
85
  }
86
86
 
87
+ // Add SessionStart-specific fields
88
+ if (context.event === "SessionStart") {
89
+ if (context.source !== undefined) {
90
+ jsonInput.source = context.source;
91
+ }
92
+ if (context.agentType !== undefined) {
93
+ jsonInput.agent_type = context.agentType;
94
+ }
95
+ }
96
+
97
+ // Add SessionEnd-specific fields
98
+ if (context.event === "SessionEnd") {
99
+ if (context.endSource !== undefined) {
100
+ jsonInput.end_source = context.endSource;
101
+ }
102
+ }
103
+
87
104
  return jsonInput;
88
105
  }
89
106
 
@@ -178,6 +178,38 @@ export class InitializationService {
178
178
  // Don't throw error to prevent app startup failure
179
179
  }
180
180
 
181
+ // Execute SessionStart hooks
182
+ try {
183
+ const phaseStart = performance.now();
184
+ const sessionStartResult = await hookManager.executeSessionStartHooks(
185
+ "startup",
186
+ messageManager.getSessionId(),
187
+ messageManager.getTranscriptPath(),
188
+ );
189
+
190
+ // Inject additionalContext as a meta user message (matches Claude Code)
191
+ if (sessionStartResult.additionalContext) {
192
+ messageManager.addUserMessage({
193
+ content: `<system-reminder>\nSessionStart hook additional context: ${sessionStartResult.additionalContext}\n</system-reminder>`,
194
+ isMeta: true,
195
+ });
196
+ }
197
+
198
+ // Inject initialUserMessage as a meta user message
199
+ if (sessionStartResult.initialUserMessage) {
200
+ messageManager.addUserMessage({
201
+ content: sessionStartResult.initialUserMessage,
202
+ isMeta: true,
203
+ });
204
+ }
205
+
206
+ logger?.debug(
207
+ `Initialization Phase [SessionStart Hooks] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
208
+ );
209
+ } catch (error) {
210
+ logger?.warn("SessionStart hooks execution failed:", error);
211
+ }
212
+
181
213
  // Trigger WorktreeCreate hook if this is a new worktree
182
214
  if (agentOptions.isNewWorktree && hookManager) {
183
215
  try {
@@ -318,7 +350,7 @@ export class InitializationService {
318
350
  if (sessionToRestore) {
319
351
  messageManager.initializeFromSession(sessionToRestore);
320
352
 
321
- // Update task manager with the root session ID to ensure continuity across compressions
353
+ // Update task manager with the root session ID to ensure continuity across compactions
322
354
  taskManager.setTaskListId(
323
355
  sessionToRestore.rootSessionId || sessionToRestore.id,
324
356
  );
@@ -183,7 +183,7 @@ export class InteractionService {
183
183
  // 6. Initialize session state last
184
184
  messageManager.initializeFromSession(sessionData);
185
185
 
186
- // Update task manager with the root session ID to ensure continuity across compressions
186
+ // Update task manager with the root session ID to ensure continuity across compactions
187
187
  taskManager.setTaskListId(sessionData.rootSessionId || sessionData.id);
188
188
 
189
189
  // 7. Load tasks for the restored session