portable-agent-layer 0.94.0 → 0.95.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/README.md +2 -0
- package/assets/templates/hooks.antigravity.json +1 -1
- package/assets/templates/hooks.codex.json +11 -1
- package/assets/templates/hooks.copilot.json +11 -4
- package/assets/templates/hooks.cursor.json +8 -2
- package/assets/templates/settings.claude.json +55 -3
- package/package.json +1 -1
- package/src/cli/content.ts +23 -0
- package/src/cli/events.ts +47 -0
- package/src/cli/tree.ts +4 -0
- package/src/hooks/AttentionEvent.ts +22 -0
- package/src/hooks/LoadContext.ts +2 -2
- package/src/hooks/PostToolOrchestrator.ts +20 -0
- package/src/hooks/PreCompactPersist.ts +4 -2
- package/src/hooks/PreserveTranscripts.ts +13 -0
- package/src/hooks/SessionClose.ts +5 -0
- package/src/hooks/UserPromptOrchestrator.ts +9 -2
- package/src/hooks/lib/attention-events.ts +43 -0
- package/src/hooks/lib/auto-update.ts +24 -1
- package/src/hooks/lib/commitment-events.ts +20 -0
- package/src/hooks/lib/discord-events.ts +25 -0
- package/src/hooks/lib/entity-events.ts +16 -0
- package/src/hooks/lib/events.ts +252 -0
- package/src/hooks/lib/export.ts +1 -1
- package/src/hooks/lib/hook-timing.ts +33 -0
- package/src/hooks/lib/inference-events.ts +56 -0
- package/src/hooks/lib/inference.ts +17 -1
- package/src/hooks/lib/interaction.ts +74 -27
- package/src/hooks/lib/opencode-events.ts +59 -0
- package/src/hooks/lib/paths.ts +1 -0
- package/src/hooks/lib/post-tool.ts +66 -0
- package/src/hooks/lib/session-events.ts +211 -0
- package/src/hooks/lib/settings.ts +6 -0
- package/src/hooks/lib/stop.ts +27 -31
- package/src/hooks/lib/tool-events.ts +106 -0
- package/src/hooks/lib/transcript-preserve.ts +133 -0
- package/src/hooks/lib/turn-extras.ts +78 -0
- package/src/hooks/lib/waiting-on-user.ts +75 -0
- package/src/hooks/lib/work-tracking.ts +17 -3
- package/src/targets/opencode/plugin.ts +33 -1
- package/src/tools/agent/interaction.ts +2 -2
- package/src/tools/agent/project.ts +4 -0
- package/src/tools/control-room/snooze.ts +2 -0
- package/src/tools/control-room/writes.ts +2 -0
- package/src/tools/knowledge/lib.ts +5 -0
- package/src/tools/lib/session-usage.ts +7 -3
- package/src/tools/lib/transcript-usage.ts +14 -0
- package/src/tools/lib/usage-buckets.ts +15 -8
- package/src/hooks/LedgerCommit.ts +0 -34
- package/src/hooks/LedgerUnapplied.ts +0 -47
package/README.md
CHANGED
|
@@ -85,6 +85,8 @@ pal cli status # check your setup
|
|
|
85
85
|
| `pal cli migrate` | Run pending data migrations (non-destructive) |
|
|
86
86
|
| `pal cli analyze [--actionable]` | Learning analysis: rating trends, failure patterns, graduation candidates |
|
|
87
87
|
| `pal cli usage` | Summarize token usage and estimated cost |
|
|
88
|
+
| `pal cli content preserve` | Copy every Claude Code transcript, gzipped, into `memory/content/` before Claude Code deletes it (30 days by default). Runs on its own whenever a session closes; the content store never leaves the machine, so `export` skips it |
|
|
89
|
+
| `pal cli events discord-nag <sent\|answered>` | For a bot outside PAL: record a nag it sent (`--kind <kind>`), or the answer to one and how long it took (`--after-sec <n>`), in the local event log |
|
|
88
90
|
| `pal cli infer` | One model call for scripts and bots, on the active agent's route: prompt on stdin, reply on stdout. `--tier small\|medium`, `--system <file>`, `--schema <file>` for a JSON reply checked against it, `--timeout <seconds>`. The call loads no PAL hooks or context; exits 1 with the reason when no route answers |
|
|
89
91
|
| `pal cli actor [label <name>]` | Show or rename the actor — who caused a record. Travels with an export, so a shared memory can tell two people apart |
|
|
90
92
|
| `pal cli machine [label <name>]` | Show or rename this install — where a record was written. Never leaves the machine |
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"hooks": [
|
|
40
40
|
{
|
|
41
41
|
"type": "command",
|
|
42
|
-
"command": "bun run {{PKG_ROOT}}/src/hooks/
|
|
42
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=codex"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
45
45
|
}
|
|
@@ -54,6 +54,16 @@
|
|
|
54
54
|
]
|
|
55
55
|
}
|
|
56
56
|
],
|
|
57
|
+
"PreCompact": [
|
|
58
|
+
{
|
|
59
|
+
"hooks": [
|
|
60
|
+
{
|
|
61
|
+
"type": "command",
|
|
62
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PreCompactPersist.ts --agent=codex"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
],
|
|
57
67
|
"SessionEnd": [
|
|
58
68
|
{
|
|
59
69
|
"hooks": [
|
|
@@ -47,15 +47,15 @@
|
|
|
47
47
|
"postToolUse": [
|
|
48
48
|
{
|
|
49
49
|
"type": "command",
|
|
50
|
-
"bash": "bun run {{PKG_ROOT}}/src/hooks/
|
|
51
|
-
"powershell": "bun run {{PKG_ROOT}}/src/hooks/
|
|
50
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=copilot",
|
|
51
|
+
"powershell": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=copilot"
|
|
52
52
|
}
|
|
53
53
|
],
|
|
54
54
|
"postToolUseFailure": [
|
|
55
55
|
{
|
|
56
56
|
"type": "command",
|
|
57
|
-
"bash": "bun run {{PKG_ROOT}}/src/hooks/
|
|
58
|
-
"powershell": "bun run {{PKG_ROOT}}/src/hooks/
|
|
57
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=copilot --event=postToolUseFailure",
|
|
58
|
+
"powershell": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=copilot --event=postToolUseFailure"
|
|
59
59
|
}
|
|
60
60
|
],
|
|
61
61
|
"agentStop": [
|
|
@@ -65,6 +65,13 @@
|
|
|
65
65
|
"powershell": "bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts --agent=copilot"
|
|
66
66
|
}
|
|
67
67
|
],
|
|
68
|
+
"preCompact": [
|
|
69
|
+
{
|
|
70
|
+
"type": "command",
|
|
71
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/PreCompactPersist.ts --agent=copilot",
|
|
72
|
+
"powershell": "bun run {{PKG_ROOT}}/src/hooks/PreCompactPersist.ts --agent=copilot"
|
|
73
|
+
}
|
|
74
|
+
],
|
|
68
75
|
"sessionEnd": [
|
|
69
76
|
{
|
|
70
77
|
"type": "command",
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
"postToolUse": [
|
|
35
35
|
{
|
|
36
36
|
"type": "command",
|
|
37
|
-
"command": "bun run {{PKG_ROOT}}/src/hooks/
|
|
37
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=cursor"
|
|
38
38
|
}
|
|
39
39
|
],
|
|
40
40
|
"postToolUseFailure": [
|
|
41
41
|
{
|
|
42
42
|
"type": "command",
|
|
43
|
-
"command": "bun run {{PKG_ROOT}}/src/hooks/
|
|
43
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=cursor"
|
|
44
44
|
}
|
|
45
45
|
],
|
|
46
46
|
"beforeShellExecution": [
|
|
@@ -61,6 +61,12 @@
|
|
|
61
61
|
"command": "bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts --agent=cursor"
|
|
62
62
|
}
|
|
63
63
|
],
|
|
64
|
+
"preCompact": [
|
|
65
|
+
{
|
|
66
|
+
"type": "command",
|
|
67
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PreCompactPersist.ts --agent=cursor"
|
|
68
|
+
}
|
|
69
|
+
],
|
|
64
70
|
"sessionEnd": [
|
|
65
71
|
{
|
|
66
72
|
"type": "command",
|
|
@@ -158,7 +158,17 @@
|
|
|
158
158
|
"hooks": [
|
|
159
159
|
{
|
|
160
160
|
"type": "command",
|
|
161
|
-
"command": "bun run {{PKG_ROOT}}/src/hooks/
|
|
161
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=claude --only=ledger"
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"matcher": "",
|
|
167
|
+
"hooks": [
|
|
168
|
+
{
|
|
169
|
+
"type": "command",
|
|
170
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=claude --only=events",
|
|
171
|
+
"async": true
|
|
162
172
|
}
|
|
163
173
|
]
|
|
164
174
|
}
|
|
@@ -169,7 +179,39 @@
|
|
|
169
179
|
"hooks": [
|
|
170
180
|
{
|
|
171
181
|
"type": "command",
|
|
172
|
-
"command": "bun run {{PKG_ROOT}}/src/hooks/
|
|
182
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=claude --only=ledger"
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"matcher": "",
|
|
188
|
+
"hooks": [
|
|
189
|
+
{
|
|
190
|
+
"type": "command",
|
|
191
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=claude --only=events",
|
|
192
|
+
"async": true
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
"PermissionRequest": [
|
|
198
|
+
{
|
|
199
|
+
"matcher": "",
|
|
200
|
+
"hooks": [
|
|
201
|
+
{
|
|
202
|
+
"type": "command",
|
|
203
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/AttentionEvent.ts --agent=claude"
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
"Notification": [
|
|
209
|
+
{
|
|
210
|
+
"matcher": "",
|
|
211
|
+
"hooks": [
|
|
212
|
+
{
|
|
213
|
+
"type": "command",
|
|
214
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/AttentionEvent.ts --agent=claude"
|
|
173
215
|
}
|
|
174
216
|
]
|
|
175
217
|
}
|
|
@@ -180,7 +222,17 @@
|
|
|
180
222
|
"hooks": [
|
|
181
223
|
{
|
|
182
224
|
"type": "command",
|
|
183
|
-
"command": "bun run {{PKG_ROOT}}/src/hooks/
|
|
225
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=claude --only=ledger"
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"matcher": "",
|
|
231
|
+
"hooks": [
|
|
232
|
+
{
|
|
233
|
+
"type": "command",
|
|
234
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=claude --only=events",
|
|
235
|
+
"async": true
|
|
184
236
|
}
|
|
185
237
|
]
|
|
186
238
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { preserveTranscriptsOnce } from "../hooks/lib/transcript-preserve";
|
|
2
|
+
import { group, leaf } from "../tools/lib/command";
|
|
3
|
+
|
|
4
|
+
function printPreserve(): undefined {
|
|
5
|
+
const result = preserveTranscriptsOnce();
|
|
6
|
+
if (!result) {
|
|
7
|
+
console.log("Another copy is already running.");
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
console.log(
|
|
11
|
+
`Copied ${result.copied}, unchanged ${result.unchanged}, failed ${result.failed}`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const contentCommand = group({
|
|
16
|
+
summary: "PAL's local content store (never exported)",
|
|
17
|
+
commands: {
|
|
18
|
+
preserve: leaf({
|
|
19
|
+
summary: "Copy Claude Code transcripts, gzipped, before Claude Code deletes them",
|
|
20
|
+
run: printPreserve,
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { isNagStage, logDiscordNag, NAG_STAGES } from "../hooks/lib/discord-events";
|
|
2
|
+
import { group, leaf, UsageError } from "../tools/lib/command";
|
|
3
|
+
|
|
4
|
+
function secondsFrom(raw: string | undefined): number | null {
|
|
5
|
+
if (raw === undefined) return null;
|
|
6
|
+
const seconds = Number(raw);
|
|
7
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
8
|
+
throw new UsageError(`--after-sec must be a number of seconds, got "${raw}"`);
|
|
9
|
+
}
|
|
10
|
+
return Math.round(seconds);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function recordDiscordNag(
|
|
14
|
+
stage: string | undefined,
|
|
15
|
+
kind: string | undefined,
|
|
16
|
+
afterSec: string | undefined
|
|
17
|
+
): undefined {
|
|
18
|
+
if (!isNagStage(stage)) {
|
|
19
|
+
throw new UsageError(`the stage is one of ${NAG_STAGES.join(", ")}`);
|
|
20
|
+
}
|
|
21
|
+
const responseSec = secondsFrom(afterSec);
|
|
22
|
+
if (stage === "answered" && responseSec === null) {
|
|
23
|
+
throw new UsageError("an answered nag needs --after-sec");
|
|
24
|
+
}
|
|
25
|
+
const id = logDiscordNag(stage, kind ?? null, responseSec);
|
|
26
|
+
console.log(id ? `Recorded ${id}` : "The event log is off; nothing recorded.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const eventsCommand = group({
|
|
30
|
+
summary: "PAL's local event log",
|
|
31
|
+
commands: {
|
|
32
|
+
"discord-nag": leaf({
|
|
33
|
+
summary: "Record a nag the Discord bot sent, or how long its answer took",
|
|
34
|
+
args: "<sent|answered>",
|
|
35
|
+
options: {
|
|
36
|
+
kind: { type: "string", value: "<kind>", description: "What the nag was about" },
|
|
37
|
+
"after-sec": {
|
|
38
|
+
type: "string",
|
|
39
|
+
value: "<n>",
|
|
40
|
+
description: "Seconds from the nag to the answer",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
run: ({ positionals, values }) =>
|
|
44
|
+
recordDiscordNag(positionals[0], values.kind, values["after-sec"]),
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
});
|
package/src/cli/tree.ts
CHANGED
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
type OptionSpecs,
|
|
15
15
|
} from "../tools/lib/command";
|
|
16
16
|
import { builtinTools, builtinToolVerbs } from "./builtin-tools";
|
|
17
|
+
import { contentCommand } from "./content";
|
|
18
|
+
import { eventsCommand } from "./events";
|
|
17
19
|
import { identityCommand } from "./identity";
|
|
18
20
|
import { inferCommand } from "./infer";
|
|
19
21
|
import { knowledgeCommand } from "./knowledge";
|
|
@@ -151,6 +153,8 @@ export function cliTree(admin: AdminHandlers): Group {
|
|
|
151
153
|
commands: {
|
|
152
154
|
...adminCommands(admin),
|
|
153
155
|
usage: usageCommand,
|
|
156
|
+
content: contentCommand,
|
|
157
|
+
events: eventsCommand,
|
|
154
158
|
infer: inferCommand,
|
|
155
159
|
actor: identityCommand("actor"),
|
|
156
160
|
machine: identityCommand("machine"),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: PermissionRequest and Notification — logs that the agent is waiting on
|
|
3
|
+
* the user. Logging only: it prints nothing, so it never answers the prompt, and
|
|
4
|
+
* always exits 0.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logAttention } from "./lib/attention-events";
|
|
8
|
+
import { duplicatesCursorHooks } from "./lib/cursor-shadow";
|
|
9
|
+
import { enterHookWorkspace } from "./lib/hook-turn";
|
|
10
|
+
import { isPalSpawnedInference } from "./lib/spawn-guard";
|
|
11
|
+
import { readStdinJSON } from "./lib/stdin";
|
|
12
|
+
|
|
13
|
+
if (isPalSpawnedInference()) process.exit(0);
|
|
14
|
+
if (duplicatesCursorHooks()) process.exit(0);
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const input = await readStdinJSON<Record<string, unknown>>();
|
|
18
|
+
enterHookWorkspace(input);
|
|
19
|
+
logAttention(input);
|
|
20
|
+
} catch {
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
package/src/hooks/LoadContext.ts
CHANGED
|
@@ -24,11 +24,10 @@ import {
|
|
|
24
24
|
isSubagentSession,
|
|
25
25
|
needsAgentsMd,
|
|
26
26
|
} from "./lib/session-context";
|
|
27
|
+
import { logSessionStart } from "./lib/session-events";
|
|
27
28
|
import { isPalSpawnedInference } from "./lib/spawn-guard";
|
|
28
29
|
import { readStdinJSON } from "./lib/stdin";
|
|
29
30
|
|
|
30
|
-
// Recursion guard — when this process is a PAL-spawned inference subprocess,
|
|
31
|
-
// skip all context loading so we don't trigger another inference call.
|
|
32
31
|
if (isPalSpawnedInference()) process.exit(0);
|
|
33
32
|
if (duplicatesCursorHooks()) process.exit(0);
|
|
34
33
|
if (isSubagentSession(process.env)) {
|
|
@@ -51,6 +50,7 @@ try {
|
|
|
51
50
|
try {
|
|
52
51
|
const active = getActiveAgent();
|
|
53
52
|
const input = await readStdinJSON<{ source?: string }>();
|
|
53
|
+
logSessionStart(input);
|
|
54
54
|
const wanted = contextForSource(active, input?.source);
|
|
55
55
|
if (wanted === "none") {
|
|
56
56
|
logDebug("LoadContext", `source=${input?.source}, context already present`);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: after a tool call, a failed call or a denied one. Silent and fail-open:
|
|
3
|
+
* it prints nothing and always exits 0, so it can never disturb a turn.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { duplicatesCursorHooks } from "./lib/cursor-shadow";
|
|
7
|
+
import { enterHookWorkspace } from "./lib/hook-turn";
|
|
8
|
+
import { runPostToolHandlers } from "./lib/post-tool";
|
|
9
|
+
import { readStdinJSON } from "./lib/stdin";
|
|
10
|
+
|
|
11
|
+
if (duplicatesCursorHooks()) process.exit(0);
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const input = await readStdinJSON<Record<string, unknown>>();
|
|
15
|
+
if (!input) process.exit(0);
|
|
16
|
+
enterHookWorkspace(input);
|
|
17
|
+
runPostToolHandlers(input);
|
|
18
|
+
} catch {
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Storage: ~/.pal/memory/state/last-exchange/{session_id}.json + latest.json.
|
|
9
9
|
* Path lives under PAL_HOME so other agents (OpenCode, Copilot, Cursor) can read the
|
|
10
|
-
* same state if/when they grow equivalent compaction-recovery hooks.
|
|
11
|
-
*
|
|
10
|
+
* same state if/when they grow equivalent compaction-recovery hooks. Cursor, Copilot
|
|
11
|
+
* and Codex run it too, which logs their compactions.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync } from "node:fs";
|
|
@@ -19,6 +19,7 @@ import { duplicatesCursorHooks } from "./lib/cursor-shadow";
|
|
|
19
19
|
import { logDebug, logError } from "./lib/log";
|
|
20
20
|
import { paths } from "./lib/paths";
|
|
21
21
|
import { sessionDir } from "./lib/session-dir";
|
|
22
|
+
import { logCompaction } from "./lib/session-events";
|
|
22
23
|
import { isPalSpawnedInference } from "./lib/spawn-guard";
|
|
23
24
|
import { readStdinJSON } from "./lib/stdin";
|
|
24
25
|
import { readTranscriptFile } from "./lib/transcript";
|
|
@@ -37,6 +38,7 @@ interface PreCompactInput {
|
|
|
37
38
|
|
|
38
39
|
const main = async () => {
|
|
39
40
|
const input = await readStdinJSON<PreCompactInput>();
|
|
41
|
+
logCompaction(input);
|
|
40
42
|
if (!input?.transcript_path) {
|
|
41
43
|
logDebug("PreCompactPersist", "No transcript_path in input — skipping");
|
|
42
44
|
process.exit(0);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawned, never awaited — copies Claude Code transcripts into PAL's content
|
|
3
|
+
* store in a process of its own, so no session waits on the gzip.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logError } from "./lib/log";
|
|
7
|
+
import { preserveTranscriptsOnce } from "./lib/transcript-preserve";
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
preserveTranscriptsOnce();
|
|
11
|
+
} catch (err) {
|
|
12
|
+
logError("PreserveTranscripts", err);
|
|
13
|
+
}
|
|
@@ -13,14 +13,19 @@
|
|
|
13
13
|
import { autoUpdateOnClose } from "./lib/auto-update";
|
|
14
14
|
import { duplicatesCursorHooks } from "./lib/cursor-shadow";
|
|
15
15
|
import { logError } from "./lib/log";
|
|
16
|
+
import { logSessionEnd } from "./lib/session-events";
|
|
16
17
|
import { isPalSpawnedInference } from "./lib/spawn-guard";
|
|
17
18
|
import { readStdinJSON } from "./lib/stdin";
|
|
19
|
+
import { spawnTranscriptPreserve } from "./lib/transcript-preserve";
|
|
18
20
|
|
|
19
21
|
if (isPalSpawnedInference()) process.exit(0);
|
|
20
22
|
if (duplicatesCursorHooks()) process.exit(0);
|
|
21
23
|
|
|
24
|
+
spawnTranscriptPreserve();
|
|
25
|
+
|
|
22
26
|
try {
|
|
23
27
|
const input = await readStdinJSON<{ reason?: string }>();
|
|
28
|
+
logSessionEnd(input);
|
|
24
29
|
autoUpdateOnClose(input?.reason);
|
|
25
30
|
} catch (err) {
|
|
26
31
|
logError("SessionClose", err);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { injectPromptContext } from "./handlers/inject-retrieval";
|
|
11
11
|
import { duplicatesCursorHooks } from "./lib/cursor-shadow";
|
|
12
|
+
import { startHookTiming } from "./lib/hook-timing";
|
|
12
13
|
import { type HookTurnPayload, hookSessionId } from "./lib/hook-turn";
|
|
13
14
|
import { logDebug, logPromptSnapshot } from "./lib/log";
|
|
14
15
|
import { capturePrompt } from "./lib/prompt-capture";
|
|
@@ -28,9 +29,15 @@ const input = await readStdinJSON<PromptSubmitInput>();
|
|
|
28
29
|
logDebug("UserPromptOrchestrator", `Input: ${JSON.stringify(input).slice(0, 200)}`);
|
|
29
30
|
if (!input?.prompt) process.exit(0);
|
|
30
31
|
|
|
32
|
+
const timing = startHookTiming("prompt");
|
|
31
33
|
const sentAt = new Date();
|
|
32
34
|
const sessionId = hookSessionId(input);
|
|
33
|
-
const injected = await
|
|
35
|
+
const injected = await timing.time("inject-context", () =>
|
|
36
|
+
injectPromptContext(input.prompt, sessionId)
|
|
37
|
+
);
|
|
34
38
|
logPromptSnapshot(input.prompt, injected);
|
|
35
39
|
|
|
36
|
-
await
|
|
40
|
+
await timing.time("capture", () =>
|
|
41
|
+
capturePrompt(input.prompt, sessionId, sentAt, "UserPromptOrchestrator")
|
|
42
|
+
);
|
|
43
|
+
timing.log(sessionId);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When the agent waits on the user: a permission prompt, or a notification that
|
|
3
|
+
* it needs them.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { normalizeToolUse } from "./agent";
|
|
7
|
+
import { appendEvent, logQuietly } from "./events";
|
|
8
|
+
import { hookSessionId } from "./hook-turn";
|
|
9
|
+
import { markWaiting } from "./waiting-on-user";
|
|
10
|
+
|
|
11
|
+
function logPermissionPrompt(
|
|
12
|
+
payload: Record<string, unknown> | null,
|
|
13
|
+
tool: string | null,
|
|
14
|
+
now: Date = new Date()
|
|
15
|
+
): void {
|
|
16
|
+
logQuietly("permission_prompt", () => {
|
|
17
|
+
const session = hookSessionId(payload) ?? null;
|
|
18
|
+
appendEvent("permission_prompt", { tool }, { ts: now.toISOString(), session });
|
|
19
|
+
if (session)
|
|
20
|
+
markWaiting("permission", session, { ts: now.toISOString(), tool: tool ?? "" });
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function logNotification(payload: Record<string, unknown> | null, now: Date): void {
|
|
25
|
+
logQuietly("notification", () => {
|
|
26
|
+
const session = hookSessionId(payload) ?? null;
|
|
27
|
+
const kind =
|
|
28
|
+
typeof payload?.notification_type === "string" ? payload.notification_type : null;
|
|
29
|
+
appendEvent("notification", { kind }, { ts: now.toISOString(), session });
|
|
30
|
+
if (session) markWaiting("notified", session, { ts: now.toISOString() });
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Claude's PermissionRequest and Notification share one hook; it prints nothing. */
|
|
35
|
+
export function logAttention(
|
|
36
|
+
payload: Record<string, unknown> | null,
|
|
37
|
+
now: Date = new Date()
|
|
38
|
+
): void {
|
|
39
|
+
const event = payload?.hook_event_name ?? payload?.hookEventName;
|
|
40
|
+
if (event === "Notification") logNotification(payload, now);
|
|
41
|
+
if (event === "PermissionRequest")
|
|
42
|
+
logPermissionPrompt(payload, normalizeToolUse(payload)?.toolName ?? null, now);
|
|
43
|
+
}
|
|
@@ -15,6 +15,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
15
15
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { resolve } from "node:path";
|
|
17
17
|
import { cachedStatus, getInstalledVersion, isRepoMode } from "../handlers/update-check";
|
|
18
|
+
import { appendEvent, logQuietly } from "./events";
|
|
18
19
|
import { logDebug, logError } from "./log";
|
|
19
20
|
import { assets, palPkg, paths } from "./paths";
|
|
20
21
|
import { raw as rawSettings } from "./settings";
|
|
@@ -118,8 +119,23 @@ function hasUncommittedChanges(): boolean {
|
|
|
118
119
|
return (status.stdout ?? "").trim().length > 0;
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
function outcomeOf(entry: AutoUpdateLedger): string {
|
|
123
|
+
if (!entry.ok) return "failed";
|
|
124
|
+
return entry.from === entry.to ? "unchanged" : "updated";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function secondsBetween(from?: string, to?: string): number | null {
|
|
128
|
+
if (!from || !to) return null;
|
|
129
|
+
return Math.round((Date.parse(to) - Date.parse(from)) / 1000);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function logUpdateEvent(fields: Record<string, unknown>): void {
|
|
133
|
+
logQuietly("pal_update", () => appendEvent("pal_update", fields));
|
|
134
|
+
}
|
|
135
|
+
|
|
121
136
|
function recordSkip(reason: string): void {
|
|
122
137
|
writeLedger({ ...readLedger(), skippedAt: new Date().toISOString(), skipped: reason });
|
|
138
|
+
logUpdateEvent({ outcome: "skipped", skipReason: reason });
|
|
123
139
|
logDebug("auto-update", `skipped: ${reason}`);
|
|
124
140
|
}
|
|
125
141
|
|
|
@@ -278,7 +294,7 @@ function update(): AutoUpdateLedger {
|
|
|
278
294
|
|
|
279
295
|
const ok = run.status === 0;
|
|
280
296
|
logDebug("auto-update", `update exited ${run.status}`);
|
|
281
|
-
|
|
297
|
+
const entry = writeLedger({
|
|
282
298
|
...readLedger(),
|
|
283
299
|
finishedAt: new Date().toISOString(),
|
|
284
300
|
ok,
|
|
@@ -286,6 +302,13 @@ function update(): AutoUpdateLedger {
|
|
|
286
302
|
to: getInstalledVersion(),
|
|
287
303
|
error: ok ? undefined : (run.error?.message ?? `exit ${run.status}`),
|
|
288
304
|
});
|
|
305
|
+
logUpdateEvent({
|
|
306
|
+
outcome: outcomeOf(entry),
|
|
307
|
+
from: entry.from ?? null,
|
|
308
|
+
to: entry.to ?? null,
|
|
309
|
+
durationSec: secondsBetween(entry.attemptedAt, entry.finishedAt),
|
|
310
|
+
});
|
|
311
|
+
return entry;
|
|
289
312
|
}
|
|
290
313
|
|
|
291
314
|
/** Hands the update to its own process, which outlives the session that closed. */
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A commitment is an ISC: completing it keeps it, retiring it drops it, and a
|
|
3
|
+
* snooze puts it off. Each change of state is one event, whichever surface made it.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { appendEvent, logQuietly } from "./events";
|
|
7
|
+
|
|
8
|
+
export type CommitmentOutcome = "kept" | "dropped" | "snoozed" | "reopened";
|
|
9
|
+
|
|
10
|
+
export function logCommitment(
|
|
11
|
+
iscProject: string,
|
|
12
|
+
isc: number,
|
|
13
|
+
outcome: CommitmentOutcome,
|
|
14
|
+
surface: "cli" | "control-room",
|
|
15
|
+
snoozeDays: number | null = null
|
|
16
|
+
): void {
|
|
17
|
+
logQuietly("commitment", () =>
|
|
18
|
+
appendEvent("commitment", { iscProject, isc, outcome, surface, snoozeDays })
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A nag the Discord bot sent, and how long the user took to answer it. The bot
|
|
3
|
+
* runs outside PAL, so it reports both through `pal cli events discord-nag`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { appendEvent } from "./events";
|
|
7
|
+
|
|
8
|
+
export const NAG_STAGES = ["sent", "answered"] as const;
|
|
9
|
+
export type NagStage = (typeof NAG_STAGES)[number];
|
|
10
|
+
|
|
11
|
+
export function isNagStage(value: unknown): value is NagStage {
|
|
12
|
+
return typeof value === "string" && (NAG_STAGES as readonly string[]).includes(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function logDiscordNag(
|
|
16
|
+
stage: NagStage,
|
|
17
|
+
kind: string | null,
|
|
18
|
+
responseSec: number | null
|
|
19
|
+
): string | null {
|
|
20
|
+
return appendEvent("discord_nag", {
|
|
21
|
+
stage,
|
|
22
|
+
kind,
|
|
23
|
+
responseSec: stage === "answered" ? responseSec : null,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first time a person, company, idea or research topic enters the knowledge
|
|
3
|
+
* store. The name stays in the content store; the event carries only its kind.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { appendEvent, logQuietly } from "./events";
|
|
7
|
+
|
|
8
|
+
export function logEntityFirstSeen(
|
|
9
|
+
domain: string,
|
|
10
|
+
entityType: string,
|
|
11
|
+
name: string
|
|
12
|
+
): void {
|
|
13
|
+
logQuietly("entity_first_seen", () =>
|
|
14
|
+
appendEvent("entity_first_seen", { domain, entityType }, { content: { name } })
|
|
15
|
+
);
|
|
16
|
+
}
|