portable-agent-layer 0.96.1 → 0.97.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.96.1",
3
+ "version": "0.97.0",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,7 @@ import { latestUserRequest } from "./antigravity-transcript";
15
15
  import { autoUpdateOnStart } from "./auto-update";
16
16
  import { regenerateIfNeeded } from "./claude-md";
17
17
  import { buildSystemReminder } from "./context";
18
+ import { startHookTiming } from "./hook-timing";
18
19
  import { type HookTurnPayload, hookSessionId } from "./hook-turn";
19
20
  import { logContextSnapshot, logError, logPromptSnapshot } from "./log";
20
21
  import { paths } from "./paths";
@@ -89,11 +90,18 @@ export async function invocationContext(
89
90
  const prompt = latestUserRequest(payload.transcriptPath);
90
91
  if (prompt === null) return null;
91
92
 
93
+ const timing = startHookTiming("prompt");
94
+ const sessionContext = claimConversationStart(conversationId)
95
+ ? await timing.time("session-context", sessionStartReminder)
96
+ : null;
92
97
  const parts = [
93
- claimConversationStart(conversationId) ? sessionStartReminder() : null,
94
- await promptReminder(prompt, conversationId),
98
+ sessionContext,
99
+ await timing.time("inject-context", () => promptReminder(prompt, conversationId)),
95
100
  ].filter((part): part is string => Boolean(part));
96
- await capturePrompt(prompt, conversationId, sentAt, "InvocationContext");
101
+ await timing.time("capture", () =>
102
+ capturePrompt(prompt, conversationId, sentAt, "InvocationContext")
103
+ );
104
+ timing.log(conversationId);
97
105
 
98
106
  return parts.length > 0 ? injectedStep(parts.join("\n\n")) : null;
99
107
  }
@@ -25,7 +25,26 @@ interface OpencodePermissionReply {
25
25
  reply?: string;
26
26
  }
27
27
 
28
+ interface OpencodeToolPart {
29
+ type?: string;
30
+ callID?: string;
31
+ sessionID?: string;
32
+ tool?: string;
33
+ state?: {
34
+ status?: string;
35
+ input?: unknown;
36
+ error?: string;
37
+ time?: { start?: number; end?: number };
38
+ };
39
+ }
40
+
41
+ // opencode's PermissionV1 error messages: RejectedError / CorrectedError for a
42
+ // prompt the user refused, DeniedError for a configured rule.
43
+ const USER_REJECTED = "The user rejected permission";
44
+ const RULE_DENIED = "The user has specified a rule which prevents";
45
+
28
46
  const askedTools = new Map<string, string>();
47
+ const loggedFailures = new Set<string>();
29
48
 
30
49
  export function logOpencodeSessionCreated(session: OpencodeSession | undefined): void {
31
50
  if (!session?.id || session.parentID) return;
@@ -57,3 +76,33 @@ export function logOpencodePermissionReply(reply: OpencodePermissionReply): void
57
76
  session_id: reply.sessionID,
58
77
  });
59
78
  }
79
+
80
+ function durationOf(part: OpencodeToolPart): number | null {
81
+ const { start, end } = part.state?.time ?? {};
82
+ return start !== undefined && end !== undefined ? end - start : null;
83
+ }
84
+
85
+ function firstFailureOf(part: OpencodeToolPart | undefined): part is OpencodeToolPart {
86
+ if (part?.type !== "tool" || part.state?.status !== "error" || !part.callID)
87
+ return false;
88
+ if (loggedFailures.has(part.callID)) return false;
89
+ loggedFailures.add(part.callID);
90
+ return true;
91
+ }
92
+
93
+ /** A tool that threw never reaches tool.execute.after; its part turning to error is the only trace. */
94
+ export function logOpencodeToolFailure(part: OpencodeToolPart | undefined): void {
95
+ if (!firstFailureOf(part)) return;
96
+ const error = part.state?.error ?? "";
97
+ if (error.startsWith(USER_REJECTED)) return;
98
+ logToolCall({
99
+ hook_event_name: error.startsWith(RULE_DENIED)
100
+ ? "PermissionDenied"
101
+ : "PostToolUseFailure",
102
+ tool_name: part.tool,
103
+ tool_input: part.state?.input,
104
+ session_id: part.sessionID,
105
+ error,
106
+ duration_ms: durationOf(part),
107
+ });
108
+ }
@@ -69,6 +69,7 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
69
69
  logOpencodeCompaction,
70
70
  logOpencodePermissionReply,
71
71
  logOpencodeSessionCreated,
72
+ logOpencodeToolFailure,
72
73
  rememberAskedTool,
73
74
  } = await lib<typeof import("../../hooks/lib/opencode-events")>("opencode-events.ts");
74
75
  const { captureSessionName } = await lib<
@@ -153,6 +154,8 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
153
154
  logOpencodeCompaction(event.properties.sessionID);
154
155
  if (event.type === "permission.replied")
155
156
  logOpencodePermissionReply(event.properties);
157
+ if (event.type === "message.part.updated")
158
+ logOpencodeToolFailure(event.properties.part);
156
159
 
157
160
  if (event.type === "session.created" || event.type === "session.updated") {
158
161
  const { regenerateIfNeeded } =