one-code-extension 0.1.4 → 0.3.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/LICENSE +20 -0
- package/README.md +222 -177
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +128 -0
- package/docs/guide/configuration.md +173 -0
- package/docs/guide/differences-from-claude-code.md +152 -0
- package/docs/guide/doctor.md +88 -0
- package/docs/guide/hooks.md +111 -0
- package/docs/guide/installation.md +126 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +452 -0
- package/docs/guide/providers-and-models.md +196 -0
- package/docs/guide/reference.md +226 -0
- package/docs/guide/sessions-and-context.md +192 -0
- package/docs/guide/skills-plugins-and-mcp.md +178 -0
- package/docs/guide/subagents-and-workflows.md +195 -0
- package/docs/guide/tasks-and-background-work.md +113 -0
- package/docs/guide/terminal-interface.md +157 -0
- package/docs/guide/tools.md +177 -0
- package/docs/guide/troubleshooting.md +177 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -4,17 +4,57 @@
|
|
|
4
4
|
* pi's `context` event; they are never written to the session file.
|
|
5
5
|
*
|
|
6
6
|
* Cross-extension contract: emit on the event bus channel
|
|
7
|
-
* `one-code:system-reminder` with `{ text, scope? }` to enqueue from
|
|
8
|
-
* extension (including third-party ones).
|
|
9
|
-
* import
|
|
7
|
+
* `one-code:system-reminder` with `{ text, scope?, placement? }` to enqueue from
|
|
8
|
+
* any extension (including third-party ones). The queue instance itself lives
|
|
9
|
+
* in the system-reminder extension; do not import one from here (jiti gives
|
|
10
|
+
* every extension its own module instance, so a shared export steers nothing).
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* Three placements, chosen by what kind of fact the reminder is:
|
|
13
|
+
*
|
|
14
|
+
* - `first-prepend` — SESSION CONTEXT that never changes mid-session (deferred
|
|
15
|
+
* tools, agent catalog, MCP instructions, skills, `# claudeMd`). Front of the
|
|
16
|
+
* FIRST user message, before the user's text, ordered by `order`, exactly
|
|
17
|
+
* Claude Code's first-message stack. Byte-stable, so the cached prefix holds.
|
|
18
|
+
* - `sticky-append` — SESSION STATE that switches on and off (auto mode, plan
|
|
19
|
+
* mode, a worktree session, ultracode). Appended to the user message that
|
|
20
|
+
* opened the turn during which the state switched on (`since`) and to EVERY
|
|
21
|
+
* user message after it, so message N carries the block on every later
|
|
22
|
+
* request too: the prefix stays byte-stable while the state holds, and the
|
|
23
|
+
* model still sees the reminder on its latest turn. Switching off drops the
|
|
24
|
+
* blocks (one cache miss, once). The old `last-append` + every-turn
|
|
25
|
+
* combination re-cached the previous turn on every turn — permanently, in
|
|
26
|
+
* auto mode.
|
|
27
|
+
* - `last-append` — ONE-SHOT STEERING about what just happened (a deferred tool
|
|
28
|
+
* miss, a file changed under the model, a mode change). Delivered where the
|
|
29
|
+
* model reads next, and then KEPT there so the message never changes again:
|
|
30
|
+
* a one-shot pending when a tool result is stored is written INTO that result
|
|
31
|
+
* (`takeOneShots` from the `tool_result` hook — Claude Code persists its
|
|
32
|
+
* mid-turn reminders the same way, so the transcript carries them and no
|
|
33
|
+
* request ever re-caches the result); a one-shot still pending when a request
|
|
34
|
+
* goes out is `pin`ned to the request's tail (the trailing tool result by call
|
|
35
|
+
* id, else the last user message by timestamp) and re-attached to that same
|
|
36
|
+
* message on every later request, byte-identical. Pins are process memory
|
|
37
|
+
* (a `--resume` drops them: one miss, once).
|
|
38
|
+
*
|
|
39
|
+
* Persisted or pinned is decided by WHEN the one-shot is emitted, which is a
|
|
40
|
+
* load-order rule: system-reminder's `tool_result` hook takes the one-shots
|
|
41
|
+
* pending at that moment, so only an extension whose own `tool_result` handler
|
|
42
|
+
* runs BEFORE it (listed before `system-reminder` in `package.json`
|
|
43
|
+
* `pi.extensions`, like `context-budget`) or that emits from `tool_call` gets
|
|
44
|
+
* its block written into the result. Emitting from a later `tool_result`
|
|
45
|
+
* handler, or from `tool_execution_end` (pi runs it AFTER the `tool_result`
|
|
46
|
+
* hooks, findings §3), lands the block on the same result but as a pin. Same
|
|
47
|
+
* wording on the wire, so this only matters for `--resume` and for ordering
|
|
48
|
+
* after the `<total_tokens>` line.
|
|
49
|
+
*
|
|
50
|
+
* A compaction summary counts as a user message for anchoring (pi renders it as
|
|
51
|
+
* one), so the context stack survives a compaction that left no user turn. So
|
|
52
|
+
* does a `custom` harness message (task notification, wakeup, hook context):
|
|
53
|
+
* pi sends it as a user message, and a turn it opens has no other user turn.
|
|
54
|
+
*
|
|
55
|
+
* Retry safety falls out of this: pi re-runs the `context` transform per LLM
|
|
56
|
+
* attempt, and both a persisted and a pinned one-shot are present on every
|
|
57
|
+
* attempt by construction.
|
|
18
58
|
*/
|
|
19
59
|
|
|
20
60
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
@@ -25,12 +65,11 @@ export const REMINDER_CHANNEL = "one-code:system-reminder";
|
|
|
25
65
|
/** "next-turn" fires once on the next LLM call; "every-turn" fires on every call until removed. */
|
|
26
66
|
export type ReminderScope = "next-turn" | "every-turn";
|
|
27
67
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
export type ReminderPlacement = "first-prepend" | "last-append";
|
|
68
|
+
/** See the module header for which placement fits which kind of reminder. */
|
|
69
|
+
export type ReminderPlacement = "first-prepend" | "sticky-append" | "last-append";
|
|
70
|
+
|
|
71
|
+
/** Where a delivered one-shot stays: a tool result (by call id) or a user turn (by timestamp). */
|
|
72
|
+
export type PinAnchor = { kind: "toolResult"; toolCallId: string } | { kind: "user"; timestamp: number };
|
|
34
73
|
|
|
35
74
|
/**
|
|
36
75
|
* `order` values for the `first-prepend` context stack, matching Claude Code's
|
|
@@ -42,9 +81,17 @@ export const CONTEXT_ORDER = {
|
|
|
42
81
|
deferredTools: 10,
|
|
43
82
|
subagentModels: 20,
|
|
44
83
|
agents: 21,
|
|
84
|
+
/** Tiny-tier-only strict delegation directive, right after the catalog. */
|
|
85
|
+
delegation: 22,
|
|
45
86
|
mcp: 30,
|
|
46
87
|
skills: 40,
|
|
88
|
+
// CLAUDE.md-family (with AGENTS.md as a per-directory fallback when a directory
|
|
89
|
+
// has no CLAUDE.md) — CLAUDE.md > AGENTS.md.
|
|
47
90
|
claudeMd: 50,
|
|
91
|
+
// One Code's own instructions ride in their own block AFTER the # claudeMd
|
|
92
|
+
// block (higher `order` = closer to the user text = higher precedence), so
|
|
93
|
+
// ONECODE.md takes precedence over CLAUDE.md/AGENTS.md. Not part of CC.
|
|
94
|
+
oneCodeMd: 60,
|
|
48
95
|
} as const;
|
|
49
96
|
|
|
50
97
|
/** A drained reminder with everything the injector needs to place it. */
|
|
@@ -58,6 +105,20 @@ export interface ReminderEntry {
|
|
|
58
105
|
* reproduces that byte-for-byte. Defaults to "".
|
|
59
106
|
*/
|
|
60
107
|
suffix?: string;
|
|
108
|
+
/**
|
|
109
|
+
* `sticky-append` only: when the state switched on. The user message that
|
|
110
|
+
* opened the turn in progress at that moment, and every user message after
|
|
111
|
+
* it, carry the block.
|
|
112
|
+
*/
|
|
113
|
+
since?: number;
|
|
114
|
+
/** Set on a pinned one-shot: the exact message it rides on every request. */
|
|
115
|
+
pin?: PinAnchor;
|
|
116
|
+
/**
|
|
117
|
+
* Emit `text` as-is instead of inside a `<system-reminder>` frame — for the
|
|
118
|
+
* few Claude Code blocks that ride bare, like `<total_tokens>` after a tool
|
|
119
|
+
* result. Defaults to false.
|
|
120
|
+
*/
|
|
121
|
+
raw?: boolean;
|
|
61
122
|
}
|
|
62
123
|
|
|
63
124
|
export interface ReminderPayload {
|
|
@@ -73,6 +134,14 @@ export interface ReminderPayload {
|
|
|
73
134
|
order?: number;
|
|
74
135
|
/** Literal text appended after the closing `</system-reminder>` tag. Defaults to "". */
|
|
75
136
|
suffix?: string;
|
|
137
|
+
/** Emit the text bare, with no `<system-reminder>` frame. */
|
|
138
|
+
raw?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* `sticky-append` only: anchor the block from this timestamp instead of now —
|
|
141
|
+
* `0` puts it on every user message in the session, including ones a resume
|
|
142
|
+
* brought back (the `<total_tokens>` line rides every user message in CC).
|
|
143
|
+
*/
|
|
144
|
+
since?: number;
|
|
76
145
|
}
|
|
77
146
|
|
|
78
147
|
interface StoredReminder extends ReminderEntry {
|
|
@@ -85,21 +154,44 @@ type EnqueueOptions = {
|
|
|
85
154
|
placement?: ReminderPlacement;
|
|
86
155
|
order?: number;
|
|
87
156
|
suffix?: string;
|
|
157
|
+
raw?: boolean;
|
|
158
|
+
/** Test seam / explicit anchor for `sticky-append`; defaults to now. */
|
|
159
|
+
since?: number;
|
|
88
160
|
};
|
|
89
161
|
|
|
90
162
|
export class ReminderQueue {
|
|
91
163
|
private nextTurn: StoredReminder[] = [];
|
|
164
|
+
/** Delivered one-shots, each fixed to the message it first rode (see `pin`). */
|
|
165
|
+
private pinned: StoredReminder[] = [];
|
|
92
166
|
private everyTurn = new Map<string, StoredReminder>();
|
|
167
|
+
private readonly now: () => number;
|
|
168
|
+
|
|
169
|
+
constructor(now: () => number = Date.now) {
|
|
170
|
+
this.now = now;
|
|
171
|
+
}
|
|
93
172
|
|
|
94
173
|
enqueue(text: string, opts?: EnqueueOptions): void {
|
|
95
174
|
if (!text.trim()) return;
|
|
175
|
+
const placement = opts?.placement ?? "last-append";
|
|
96
176
|
const entry: StoredReminder = {
|
|
97
177
|
text,
|
|
98
|
-
placement
|
|
178
|
+
placement,
|
|
99
179
|
order: opts?.order ?? 0,
|
|
100
180
|
suffix: opts?.suffix,
|
|
101
181
|
key: opts?.key,
|
|
182
|
+
raw: opts?.raw,
|
|
102
183
|
};
|
|
184
|
+
if (placement === "sticky-append") {
|
|
185
|
+
// A standing reminder re-emitted with the SAME text (plan mode re-emits
|
|
186
|
+
// every turn) keeps its anchor, so the blocks on earlier messages do not
|
|
187
|
+
// move. Different text under the same key is a new fact (plan → auto
|
|
188
|
+
// under the shared "permission-mode" key) and anchors from now.
|
|
189
|
+
const previous = opts?.key !== undefined ? this.everyTurn.get(opts.key) : undefined;
|
|
190
|
+
entry.since =
|
|
191
|
+
opts?.since ??
|
|
192
|
+
(previous?.placement === "sticky-append" && previous.text === text ? previous.since : undefined) ??
|
|
193
|
+
this.now();
|
|
194
|
+
}
|
|
103
195
|
if (opts?.scope === "every-turn") {
|
|
104
196
|
this.everyTurn.set(opts.key ?? text, entry);
|
|
105
197
|
} else {
|
|
@@ -115,98 +207,259 @@ export class ReminderQueue {
|
|
|
115
207
|
this.everyTurn.delete(key);
|
|
116
208
|
}
|
|
117
209
|
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Take the pending `last-append` one-shots out of the queue — for the
|
|
212
|
+
* `tool_result` hook, which writes them into the stored result. Other
|
|
213
|
+
* placements stay queued for the next request.
|
|
214
|
+
*/
|
|
215
|
+
takeOneShots(): ReminderEntry[] {
|
|
216
|
+
const taken = this.nextTurn.filter((r) => r.placement === "last-append");
|
|
217
|
+
this.nextTurn = this.nextTurn.filter((r) => r.placement !== "last-append");
|
|
218
|
+
return taken.map(strip);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Fix the pending `last-append` one-shots to `anchor` — the message they ride
|
|
223
|
+
* on this request — so every later request re-attaches them there unchanged.
|
|
224
|
+
* Called by the owner at `context` time, after it has decided the tail.
|
|
225
|
+
*/
|
|
226
|
+
pin(anchor: PinAnchor): void {
|
|
227
|
+
for (const entry of this.nextTurn) {
|
|
228
|
+
if (entry.placement !== "last-append") continue;
|
|
229
|
+
this.pinned.push({ ...entry, pin: anchor });
|
|
230
|
+
}
|
|
231
|
+
this.nextTurn = this.nextTurn.filter((r) => r.placement !== "last-append");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Everything to inject on this request: every-turn state, pinned one-shots,
|
|
236
|
+
* then whatever is still pending. `messages` is the request being built:
|
|
237
|
+
* pins whose anchor is no longer in it (compacted away, or left behind on
|
|
238
|
+
* another branch) are dropped here, as part of draining, so no caller can
|
|
239
|
+
* forget the step. Nothing else is ever evicted: a pin is one small object,
|
|
240
|
+
* and removing a pin whose message is still in context would change that
|
|
241
|
+
* message and re-cache everything after it — the old "oldest past 400" cap
|
|
242
|
+
* did exactly that (CACHE-REVIEW-2026-09-04 M3).
|
|
243
|
+
*/
|
|
244
|
+
drain(messages: AgentMessage[]): ReminderEntry[] {
|
|
245
|
+
if (this.pinned.length > 0) {
|
|
246
|
+
const locate = pinLocator(messages);
|
|
247
|
+
this.pinned = this.pinned.filter((entry) => locate(entry.pin as PinAnchor) !== -1);
|
|
248
|
+
}
|
|
249
|
+
const pending = this.nextTurn.map(strip);
|
|
124
250
|
this.nextTurn = [];
|
|
125
|
-
return
|
|
251
|
+
return [...[...this.everyTurn.values()].map(strip), ...this.pinned.map(strip), ...pending];
|
|
126
252
|
}
|
|
127
253
|
|
|
128
254
|
get size(): number {
|
|
129
|
-
return this.everyTurn.size + this.nextTurn.length;
|
|
255
|
+
return this.everyTurn.size + this.nextTurn.length + this.pinned.length;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** True when a one-shot is waiting to be delivered (the owner decides where). */
|
|
259
|
+
get hasPendingOneShots(): boolean {
|
|
260
|
+
return this.nextTurn.some((r) => r.placement === "last-append");
|
|
130
261
|
}
|
|
131
262
|
}
|
|
132
263
|
|
|
133
264
|
function strip(r: StoredReminder): ReminderEntry {
|
|
134
|
-
|
|
265
|
+
const entry: ReminderEntry = { text: r.text, placement: r.placement, order: r.order, suffix: r.suffix };
|
|
266
|
+
if (r.raw) entry.raw = true;
|
|
267
|
+
if (r.since !== undefined) entry.since = r.since;
|
|
268
|
+
if (r.pin !== undefined) entry.pin = r.pin;
|
|
269
|
+
return entry;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* One pass over `messages` → a lookup from a pin to the index of the message it
|
|
274
|
+
* rides (-1 when that message left the context). Built once per request by the
|
|
275
|
+
* queue's drain and once by injectReminders, so pin count never multiplies the
|
|
276
|
+
* message count on the per-request path.
|
|
277
|
+
*/
|
|
278
|
+
function pinLocator(messages: AgentMessage[]): (pin: PinAnchor) => number {
|
|
279
|
+
const byToolCall = new Map<string, number>();
|
|
280
|
+
const byTimestamp = new Map<number, number>();
|
|
281
|
+
messages.forEach((m, index) => {
|
|
282
|
+
if (m.role === "toolResult") {
|
|
283
|
+
const id = (m as { toolCallId?: string }).toolCallId;
|
|
284
|
+
if (typeof id === "string" && !byToolCall.has(id)) byToolCall.set(id, index);
|
|
285
|
+
} else if (isUserLike(m)) {
|
|
286
|
+
const stamp = (m as { timestamp?: number }).timestamp;
|
|
287
|
+
if (typeof stamp === "number" && !byTimestamp.has(stamp)) byTimestamp.set(stamp, index);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
return (pin) => (pin.kind === "toolResult" ? byToolCall.get(pin.toolCallId) : byTimestamp.get(pin.timestamp)) ?? -1;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** The anchor a one-shot lands on for this request: the trailing tool result, else the last user-like message. */
|
|
294
|
+
export function tailAnchor(messages: AgentMessage[]): PinAnchor | undefined {
|
|
295
|
+
const last = messages[messages.length - 1] as (AgentMessage & { toolCallId?: string }) | undefined;
|
|
296
|
+
if (last?.role === "toolResult" && typeof last.toolCallId === "string") return { kind: "toolResult", toolCallId: last.toolCallId };
|
|
297
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
298
|
+
const m = messages[i] as AgentMessage & { timestamp?: number };
|
|
299
|
+
if (isUserLike(m) && typeof m.timestamp === "number") {
|
|
300
|
+
return { kind: "user", timestamp: m.timestamp };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return undefined;
|
|
135
304
|
}
|
|
136
305
|
|
|
137
|
-
/**
|
|
138
|
-
export
|
|
306
|
+
/** A tool result's content with reminder blocks appended (for the `tool_result` hook). */
|
|
307
|
+
export function appendReminderBlocks(content: ContentBlock[], entries: ReminderEntry[]): ContentBlock[] {
|
|
308
|
+
return [...content, ...entries.map(reminderBlock)];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The framings a reminder/countdown block appended onto a tool result begins
|
|
313
|
+
* with: a `<system-reminder>`-wrapped one-shot, or a raw one-shot (the
|
|
314
|
+
* context-budget `<total_tokens>` countdown, an LSP `<new-diagnostics>` block).
|
|
315
|
+
* A PostToolUse hook that replaces the whole result must re-attach the trailing
|
|
316
|
+
* run of these, or it silently drops the harness's own context (review M3).
|
|
317
|
+
*/
|
|
318
|
+
const APPENDED_REMINDER_PREFIXES = ["<system-reminder>", "<total_tokens>", "<new-diagnostics>"];
|
|
319
|
+
|
|
320
|
+
/** True when a text block is one the reminder queue appended onto a tool result. */
|
|
321
|
+
export function isAppendedReminderText(text: string): boolean {
|
|
322
|
+
return APPENDED_REMINDER_PREFIXES.some((prefix) => text.startsWith(prefix));
|
|
323
|
+
}
|
|
139
324
|
|
|
140
325
|
export function wrapReminder(text: string): string {
|
|
141
326
|
return `<system-reminder>\n${text}\n</system-reminder>`;
|
|
142
327
|
}
|
|
143
328
|
|
|
329
|
+
/**
|
|
330
|
+
* pi's `convertToLlm` renders a `compactionSummary` message as a user message
|
|
331
|
+
* with exactly this frame (`core/messages.js`, not exported through the
|
|
332
|
+
* package's exports map — copied here and locked by a unit test). Used when a
|
|
333
|
+
* reminder must anchor to the summary because no user turn survived compaction.
|
|
334
|
+
*/
|
|
335
|
+
export const COMPACTION_SUMMARY_PREFIX =
|
|
336
|
+
"The conversation history before this point was compacted into the following summary:\n\n<summary>\n";
|
|
337
|
+
export const COMPACTION_SUMMARY_SUFFIX = "\n</summary>";
|
|
338
|
+
|
|
144
339
|
type ContentBlock = TextContent | ImageContent;
|
|
145
|
-
type UserMessage = AgentMessage & { content: string | ContentBlock[] };
|
|
146
340
|
|
|
147
341
|
function toBlocks(content: string | ContentBlock[]): ContentBlock[] {
|
|
148
342
|
return typeof content === "string" ? [{ type: "text", text: content }] : [...content];
|
|
149
343
|
}
|
|
150
344
|
|
|
151
345
|
function reminderBlock(entry: ReminderEntry): TextContent {
|
|
152
|
-
return { type: "text", text: wrapReminder(entry.text) + (entry.suffix ?? "") };
|
|
346
|
+
return { type: "text", text: (entry.raw ? entry.text : wrapReminder(entry.text)) + (entry.suffix ?? "") };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Roles a reminder block can be attached to. `custom` is a harness message
|
|
351
|
+
* (task notification, wakeup, hook context, `<new-diagnostics>`); pi's
|
|
352
|
+
* `convertToLlm` sends it to the model as `role: "user"`, so on the wire it IS
|
|
353
|
+
* a user turn — and a turn opened by one (a `/loop` tick, an agent report
|
|
354
|
+
* arriving while idle) has no `user` message of its own. Before 2026-09-05 such
|
|
355
|
+
* a request got no context stack at all, and its pending one-shots pinned to
|
|
356
|
+
* the previous user message (STEERING-REVIEW-2026-09-05 H1).
|
|
357
|
+
*/
|
|
358
|
+
function isUserLike(m: AgentMessage): boolean {
|
|
359
|
+
return isStickyCarrier(m) || m.role === "compactionSummary";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** User-role messages that carry sticky blocks: real turns and harness messages, not a compaction summary. */
|
|
363
|
+
function isStickyCarrier(m: AgentMessage): boolean {
|
|
364
|
+
return m.role === "user" || m.role === "custom";
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** A copy of `message` with `before` blocks in front of its content and `after` blocks behind. */
|
|
368
|
+
function withBlocks(message: AgentMessage, before: TextContent[], after: TextContent[]): AgentMessage {
|
|
369
|
+
if (before.length === 0 && after.length === 0) return message;
|
|
370
|
+
if (message.role === "compactionSummary") {
|
|
371
|
+
const summary = message as unknown as { summary: string; timestamp: number };
|
|
372
|
+
return {
|
|
373
|
+
role: "user",
|
|
374
|
+
content: [
|
|
375
|
+
...before,
|
|
376
|
+
{ type: "text", text: COMPACTION_SUMMARY_PREFIX + summary.summary + COMPACTION_SUMMARY_SUFFIX },
|
|
377
|
+
...after,
|
|
378
|
+
],
|
|
379
|
+
timestamp: summary.timestamp,
|
|
380
|
+
} as AgentMessage;
|
|
381
|
+
}
|
|
382
|
+
const target = message as AgentMessage & { content: string | ContentBlock[] };
|
|
383
|
+
return { ...target, content: [...before, ...toBlocks(target.content), ...after] } as AgentMessage;
|
|
153
384
|
}
|
|
154
385
|
|
|
155
386
|
/**
|
|
156
387
|
* Returns a copy of `messages` with reminder blocks injected (Claude Code
|
|
157
|
-
* convention: reminders ride inside
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
388
|
+
* convention: reminders ride inside message content blocks, never as messages
|
|
389
|
+
* of their own). See the module header for the three placements. A bare string
|
|
390
|
+
* is `last-append` (back-compat). The input array and its messages are not
|
|
391
|
+
* mutated. With nothing to anchor to (no user, tool-result, or compaction
|
|
392
|
+
* summary message), messages are returned unchanged and the caller keeps the
|
|
393
|
+
* reminders queued.
|
|
163
394
|
*/
|
|
164
|
-
export function injectReminders(
|
|
165
|
-
messages: AgentMessage[],
|
|
166
|
-
reminders: Array<string | ReminderEntry>,
|
|
167
|
-
): AgentMessage[] {
|
|
395
|
+
export function injectReminders(messages: AgentMessage[], reminders: Array<string | ReminderEntry>): AgentMessage[] {
|
|
168
396
|
if (reminders.length === 0) return messages;
|
|
169
397
|
|
|
170
398
|
const entries: ReminderEntry[] = reminders.map((r) =>
|
|
171
399
|
typeof r === "string" ? { text: r, placement: "last-append", order: 0 } : r,
|
|
172
400
|
);
|
|
173
|
-
|
|
174
401
|
const firstPrepend = entries
|
|
175
402
|
.filter((e) => e.placement === "first-prepend")
|
|
176
403
|
.map((e, i) => ({ e, i }))
|
|
177
404
|
.sort((a, b) => a.e.order - b.e.order || a.i - b.i)
|
|
178
405
|
.map((x) => x.e);
|
|
179
|
-
const
|
|
406
|
+
const sticky = entries.filter((e) => e.placement === "sticky-append");
|
|
407
|
+
const pinnedEntries = entries.filter((e) => e.pin !== undefined);
|
|
408
|
+
const lastAppend = entries.filter((e) => e.placement === "last-append" && e.pin === undefined);
|
|
180
409
|
|
|
181
|
-
const firstUserIndex = messages.findIndex(
|
|
182
|
-
|
|
183
|
-
const
|
|
410
|
+
const firstUserIndex = messages.findIndex(isUserLike);
|
|
411
|
+
const lastUserIndex = messages.findLastIndex(isUserLike);
|
|
412
|
+
const lastIndex = messages.length - 1;
|
|
413
|
+
// Mid-turn, the request ends in the tool result(s) the model is about to
|
|
414
|
+
// read — that is where a one-shot reminder belongs. Otherwise the last user turn.
|
|
415
|
+
const tailIndex = lastIndex >= 0 && messages[lastIndex].role === "toolResult" ? lastIndex : lastUserIndex;
|
|
416
|
+
if (firstUserIndex === -1 && tailIndex === -1) return messages;
|
|
184
417
|
|
|
185
|
-
const
|
|
418
|
+
const before = new Map<number, TextContent[]>();
|
|
419
|
+
const after = new Map<number, TextContent[]>();
|
|
420
|
+
const push = (map: Map<number, TextContent[]>, index: number, blocks: TextContent[]) => {
|
|
421
|
+
if (index < 0 || blocks.length === 0) return;
|
|
422
|
+
map.set(index, [...(map.get(index) ?? []), ...blocks]);
|
|
423
|
+
};
|
|
186
424
|
|
|
187
|
-
|
|
188
|
-
const target = result[firstUserIndex] as UserMessage;
|
|
189
|
-
const blocks = toBlocks(target.content);
|
|
190
|
-
result[firstUserIndex] = {
|
|
191
|
-
...target,
|
|
192
|
-
content: [...firstPrepend.map(reminderBlock), ...blocks, ...lastAppend.map(reminderBlock)],
|
|
193
|
-
} as AgentMessage;
|
|
194
|
-
return result;
|
|
195
|
-
}
|
|
425
|
+
push(before, firstUserIndex === -1 ? tailIndex : firstUserIndex, firstPrepend.map(reminderBlock));
|
|
196
426
|
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
427
|
+
for (const entry of sticky) {
|
|
428
|
+
const since = entry.since ?? 0;
|
|
429
|
+
// The block rides the user message that opened the turn during which the
|
|
430
|
+
// state switched on (the latest user message stamped before `since` — a
|
|
431
|
+
// standing reminder emitted on before_agent_start is always stamped after
|
|
432
|
+
// the turn's user message) and every user message after it. The set only
|
|
433
|
+
// grows while the state holds, so earlier messages never change.
|
|
434
|
+
let opener = -1;
|
|
435
|
+
const carriers: number[] = [];
|
|
436
|
+
messages.forEach((m, index) => {
|
|
437
|
+
if (!isStickyCarrier(m)) return;
|
|
438
|
+
const stamp = (m as { timestamp?: number }).timestamp ?? 0;
|
|
439
|
+
if (stamp >= since) carriers.push(index);
|
|
440
|
+
else opener = index;
|
|
441
|
+
});
|
|
442
|
+
if (opener !== -1) carriers.unshift(opener);
|
|
443
|
+
if (carriers.length === 0) {
|
|
444
|
+
// No user turn at all (overflow compaction mid-turn): ride the tail.
|
|
445
|
+
push(after, tailIndex === -1 ? firstUserIndex : tailIndex, [reminderBlock(entry)]);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
for (const index of carriers) push(after, index, [reminderBlock(entry)]);
|
|
203
449
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
450
|
+
|
|
451
|
+
push(after, tailIndex === -1 ? firstUserIndex : tailIndex, lastAppend.map(reminderBlock));
|
|
452
|
+
|
|
453
|
+
// Pinned one-shots ride the exact message they first landed on; a message
|
|
454
|
+
// compacted away simply no longer carries its pin.
|
|
455
|
+
if (pinnedEntries.length > 0) {
|
|
456
|
+
const locate = pinLocator(messages);
|
|
457
|
+
for (const entry of pinnedEntries) {
|
|
458
|
+
const index = locate(entry.pin as PinAnchor);
|
|
459
|
+
if (index !== -1) push(after, index, [reminderBlock(entry)]);
|
|
460
|
+
}
|
|
210
461
|
}
|
|
211
|
-
|
|
462
|
+
|
|
463
|
+
if (before.size === 0 && after.size === 0) return messages;
|
|
464
|
+
return messages.map((m, index) => withBlocks(m, before.get(index) ?? [], after.get(index) ?? []));
|
|
212
465
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Session scratchpad —
|
|
2
|
+
* Session scratchpad — a designated temp-files directory.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The shape follows Claude Code's (`<tmp>/<owner>/<project-slug>/<session-id>/scratchpad`),
|
|
5
|
+
* but the owner segment carries One Code's name, not Claude Code's, so a machine
|
|
6
|
+
* running both products never interleaves their scratchpads under one owner dir
|
|
7
|
+
* (distribution review 2026-09-09, H2). We tell the model (via a system-prompt
|
|
8
|
+
* section) to use it for everything that would otherwise land in `/tmp` or leak
|
|
9
|
+
* into the project. The path is per-session, so parallel sessions on one project
|
|
10
|
+
* never collide.
|
|
8
11
|
*
|
|
9
12
|
* Three extensions need the same path (system-prompt renders the section,
|
|
10
13
|
* permissions and the workflow gate allow writes into it), and jiti isolates
|
|
@@ -17,14 +20,14 @@ import { join } from "node:path";
|
|
|
17
20
|
import { findGitRoot } from "./git.ts";
|
|
18
21
|
import { projectSlug } from "./memory.ts";
|
|
19
22
|
|
|
20
|
-
/** Pure core, testable:
|
|
23
|
+
/** Pure core, testable: Claude Code's path shape under a One Code-named owner dir. */
|
|
21
24
|
export function scratchpadDir(
|
|
22
25
|
tmpRoot: string,
|
|
23
26
|
uid: number | undefined,
|
|
24
27
|
projectRoot: string,
|
|
25
28
|
sessionId: string,
|
|
26
29
|
): string {
|
|
27
|
-
const owner = uid === undefined ? "
|
|
30
|
+
const owner = uid === undefined ? "onecode" : `onecode-${uid}`;
|
|
28
31
|
return join(tmpRoot, owner, projectSlug(projectRoot), sessionId, "scratchpad");
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -34,7 +37,7 @@ export function scratchpadDir(
|
|
|
34
37
|
* resolved subject all name the same real location. Falls back to os.tmpdir()
|
|
35
38
|
* where /tmp does not exist.
|
|
36
39
|
*/
|
|
37
|
-
|
|
40
|
+
function resolveTmpRoot(): string {
|
|
38
41
|
try {
|
|
39
42
|
return realpathSync("/tmp");
|
|
40
43
|
} catch {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Is this extension instance's session still alive?" — the one flag every
|
|
3
|
+
* extension with late work (a child's `close`, a debounce, a panel action's
|
|
4
|
+
* continuation) needs. After `session_shutdown` (which `/clear`, `/new`,
|
|
5
|
+
* `/resume` and process exit all emit — findings §8) every `pi.*` call and
|
|
6
|
+
* every getter on a captured ctx throws; a callback that fires afterwards must
|
|
7
|
+
* do nothing. Before this module each extension kept its own `shuttingDown`
|
|
8
|
+
* boolean wired to the same two events (LIFECYCLE-REVIEW-2026-09-06 fix pass).
|
|
9
|
+
*
|
|
10
|
+
* Call it FIRST in the factory: its handlers register before the extension's
|
|
11
|
+
* own, so `alive()` already reads false inside the extension's
|
|
12
|
+
* `session_shutdown` handler and true again inside its `session_start` one
|
|
13
|
+
* (RPC's `new_session` re-runs factories and emits `session_start` twice;
|
|
14
|
+
* the reset is idempotent).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
|
|
19
|
+
export function sessionAlive(pi: Pick<ExtensionAPI, "on">): () => boolean {
|
|
20
|
+
let alive = true;
|
|
21
|
+
pi.on("session_start", () => {
|
|
22
|
+
alive = true;
|
|
23
|
+
});
|
|
24
|
+
pi.on("session_shutdown", () => {
|
|
25
|
+
alive = false;
|
|
26
|
+
});
|
|
27
|
+
return () => alive;
|
|
28
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-extension "a setting you own just changed on disk" channels (pure
|
|
3
|
+
* constants). jiti isolates module state, so when one extension writes another
|
|
4
|
+
* extension's setting — `/doctor preset` writing the subagent default and the
|
|
5
|
+
* auto-mode classifier model — it announces the write here and the owner
|
|
6
|
+
* re-reads its file and republishes its live status. No payload: the owner's
|
|
7
|
+
* own loader is the source of truth.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** `subagentModel` in ~/.onecode/settings.json changed; the subagents extension re-resolves and re-emits its status. */
|
|
11
|
+
export const SUBAGENT_DEFAULT_CHANGED_CHANNEL = "one-code:subagent-default-changed";
|
|
12
|
+
|
|
13
|
+
/** `autoMode.classifierModel` changed; the permissions extension drops its pinned classifier and cached config. */
|
|
14
|
+
export const CLASSIFIER_SETTING_CHANGED_CHANNEL = "one-code:classifier-setting-changed";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell-task tracker — a per-extension live mirror of the background bash
|
|
3
|
+
* shells (kind "bash") that register over TASK_REGISTER_CHANNEL.
|
|
4
|
+
*
|
|
5
|
+
* Module state does not cross extension boundaries (findings §3), but the
|
|
6
|
+
* task OBJECTS do: the bash extension emits each BackgroundTask over the bus,
|
|
7
|
+
* so any consumer holding the reference sees live status/output. Each
|
|
8
|
+
* interested extension (subagents' shell panel, turn-duration) instantiates
|
|
9
|
+
* its own tracker; multiple bus listeners are fine.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { TASK_REGISTER_CHANNEL, type BackgroundTask } from "../background/registry.ts";
|
|
13
|
+
|
|
14
|
+
export interface ShellTaskTracker {
|
|
15
|
+
/** Every bash shell registered this session, registration order. */
|
|
16
|
+
list(): BackgroundTask[];
|
|
17
|
+
running(): BackgroundTask[];
|
|
18
|
+
/** Fires on register and on any tracked shell finishing. */
|
|
19
|
+
subscribe(listener: () => void): () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface EventBusLike {
|
|
23
|
+
events: { on(channel: string, handler: (payload: unknown) => void): unknown };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function trackShellTasks(pi: EventBusLike): ShellTaskTracker {
|
|
27
|
+
const tasks = new Map<string, BackgroundTask>();
|
|
28
|
+
const listeners = new Set<() => void>();
|
|
29
|
+
const notify = () => {
|
|
30
|
+
for (const listener of listeners) {
|
|
31
|
+
try {
|
|
32
|
+
listener();
|
|
33
|
+
} catch {
|
|
34
|
+
// One consumer's render error must not starve the others.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
pi.events.on(TASK_REGISTER_CHANNEL, (payload) => {
|
|
39
|
+
const task = payload as BackgroundTask | undefined;
|
|
40
|
+
if (!task?.id || task.kind !== "bash") return;
|
|
41
|
+
tasks.set(task.id, task);
|
|
42
|
+
notify();
|
|
43
|
+
// `finished` settles (never rejects) per the BackgroundTask contract.
|
|
44
|
+
void task.finished.then(notify);
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
list: () => [...tasks.values()],
|
|
48
|
+
running: () => [...tasks.values()].filter((t) => t.status === "running"),
|
|
49
|
+
subscribe: (listener) => {
|
|
50
|
+
listeners.add(listener);
|
|
51
|
+
return () => listeners.delete(listener);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|