one-code-extension 0.1.3 → 0.3.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/LICENSE +20 -0
- package/README.md +200 -172
- 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 +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -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 -202
- 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
|
@@ -5,15 +5,19 @@
|
|
|
5
5
|
* Owns the BackgroundRegistry. Other extensions (subagents) register their
|
|
6
6
|
* long-running work over TASK_REGISTER_CHANNEL at runtime, so task_output and
|
|
7
7
|
* task_stop address every background task in the session regardless of which
|
|
8
|
-
* extension started it. Events and completions are delivered as
|
|
9
|
-
*
|
|
8
|
+
* extension started it. Events and completions are delivered as steered
|
|
9
|
+
* system notifications (lib/notifications.ts), never as user input.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { spawn } from "node:child_process";
|
|
13
13
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { Type } from "typebox";
|
|
15
|
+
import { whenAborted } from "../lib/abort.ts";
|
|
15
16
|
import { DEFER_CHANNEL } from "../lib/deferred.ts";
|
|
16
|
-
import {
|
|
17
|
+
import { persistIfLarge, sessionResultsDir } from "../lib/persisted-output.ts";
|
|
18
|
+
import { detachedSpawnOptions, KILL_GRACE_MS, stopProcessTree } from "../lib/process-tree.ts";
|
|
19
|
+
import { sessionAlive } from "../lib/session-lifecycle.ts";
|
|
20
|
+
import { ccToolRenderers, customMessageText, liveUiCtx, notificationComponent } from "../lib/tui-render.ts";
|
|
17
21
|
import {
|
|
18
22
|
type BackgroundTask,
|
|
19
23
|
BackgroundRegistry,
|
|
@@ -21,55 +25,101 @@ import {
|
|
|
21
25
|
generateTaskId,
|
|
22
26
|
TASK_REGISTER_CHANNEL,
|
|
23
27
|
} from "./registry.ts";
|
|
24
|
-
import {
|
|
25
|
-
|
|
28
|
+
import {
|
|
29
|
+
buildDynamicLoopPrompt,
|
|
30
|
+
buildLoopMessage,
|
|
31
|
+
buildWakeupMessage,
|
|
32
|
+
clampDelaySeconds,
|
|
33
|
+
describeSchedule,
|
|
34
|
+
MAX_DELAY_SECONDS,
|
|
35
|
+
MIN_DELAY_SECONDS,
|
|
36
|
+
parseLoopArgs,
|
|
37
|
+
} from "./wakeup.ts";
|
|
38
|
+
import { createTaskNotifier, oneShotNote, sessionOutlivesTurn, systemNotification } from "../lib/notifications.ts";
|
|
39
|
+
import {
|
|
40
|
+
batchSize,
|
|
41
|
+
emptyBatch,
|
|
42
|
+
formatMonitorBatch,
|
|
43
|
+
MONITOR_BATCH_BUSY_MS,
|
|
44
|
+
MONITOR_BATCH_IDLE_MS,
|
|
45
|
+
type MonitorBatch,
|
|
46
|
+
pushEvent,
|
|
47
|
+
} from "./monitor-batch.ts";
|
|
26
48
|
|
|
27
49
|
const OUTPUT_CAP = 30_000;
|
|
28
50
|
const STORED_OUTPUT_CAP = 200_000;
|
|
29
|
-
const EVENT_BATCH_MS = 1000;
|
|
30
51
|
const DEFAULT_MONITOR_TIMEOUT_MS = 300_000;
|
|
31
52
|
const MAX_MONITOR_TIMEOUT_MS = 3_600_000;
|
|
32
53
|
const MAX_BLOCK_TIMEOUT_MS = 600_000;
|
|
33
|
-
|
|
34
54
|
function tail(text: string, cap: number): string {
|
|
35
55
|
return text.length <= cap ? text : `… (earlier output truncated)\n${text.slice(-cap)}`;
|
|
36
56
|
}
|
|
37
57
|
|
|
58
|
+
/**
|
|
59
|
+
* `/clear`, `/new` and `/resume` stop every background task with the session
|
|
60
|
+
* they belong to (the notifier that would report them is inert by then, and
|
|
61
|
+
* the new session has no handle on them). Say so, once, in the NEW session:
|
|
62
|
+
* the old session's UI is torn down right after `session_shutdown`, so a
|
|
63
|
+
* notice printed there is lost. Module scope on purpose — jiti keeps this
|
|
64
|
+
* module across the factory re-run that builds the new instance, which is the
|
|
65
|
+
* only thing that survives the swap (LIFECYCLE-REVIEW-2026-09-06 L3).
|
|
66
|
+
*/
|
|
67
|
+
let pendingShutdownNotice: string | undefined;
|
|
68
|
+
|
|
38
69
|
export default function backgroundExtension(pi: ExtensionAPI) {
|
|
70
|
+
// After session_shutdown the captured ctx throws on every access and the
|
|
71
|
+
// notifier is inert; a monitor whose command ends later (its shell was
|
|
72
|
+
// killed, the orphaned command finished) must repaint nothing and say
|
|
73
|
+
// nothing — before this guard, the widget repaint from the child's `close`
|
|
74
|
+
// callback took the whole process down (LIFECYCLE-REVIEW-2026-09-06 H1).
|
|
75
|
+
const alive = sessionAlive(pi);
|
|
39
76
|
const registry = new BackgroundRegistry();
|
|
40
77
|
let lastCtx: ExtensionContext | undefined;
|
|
41
78
|
let wakeup: { timer: NodeJS.Timeout; prompt: string; reason: string } | undefined;
|
|
79
|
+
// Fixed-interval /loop (harness-driven, auto-re-arming — distinct from the
|
|
80
|
+
// model-driven `wakeup` used by dynamic /loop). One at a time, like wakeup.
|
|
81
|
+
let loop: { timer: NodeJS.Timeout; intervalSeconds: number; task: string } | undefined;
|
|
82
|
+
// True between agent_start and agent_settled, so a fixed-interval tick can skip
|
|
83
|
+
// (not queue) while the previous tick's turn is still running — no backlog.
|
|
84
|
+
// agent_settled, not agent_end: agent_end can precede an internal retry /
|
|
85
|
+
// auto-compaction / queued continuation, and a tick landing in that gap would
|
|
86
|
+
// inject a turn while the prior one is still about to resume.
|
|
87
|
+
let agentBusy = false;
|
|
88
|
+
// Set when a dynamic /loop force-activated schedule_wakeup, so /loop stop can
|
|
89
|
+
// restore the lean tool surface it changed.
|
|
90
|
+
let activatedWakeupTool = false;
|
|
42
91
|
|
|
43
92
|
pi.events.on(TASK_REGISTER_CHANNEL, (task) => registry.register(task as BackgroundTask));
|
|
44
93
|
|
|
45
94
|
const updateWidget = () => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
95
|
+
// liveUiCtx: a stale ctx (session replaced) reads as "no UI", never a throw;
|
|
96
|
+
// session_shutdown also drops lastCtx, so a late repaint is a no-op.
|
|
97
|
+
const live = liveUiCtx(lastCtx);
|
|
98
|
+
if (!live) return;
|
|
99
|
+
// A task whose producer gives it first-class panel UI (bash shells and
|
|
100
|
+
// subagent runs in the subagents panel) is excluded, or this line would
|
|
101
|
+
// stay permanently lit next to the panel already showing the same work.
|
|
102
|
+
const running = registry.running().filter((t) => !t.ownUI).length;
|
|
103
|
+
live.ui.setWidget("cc-background", running > 0 ? [` background tasks: ${running} running`] : undefined);
|
|
49
104
|
};
|
|
50
105
|
|
|
51
106
|
// Harness-injected notifications carry anti-confabulation framing for the
|
|
52
107
|
// model; the transcript shows a compact headline instead (ctrl+o expands).
|
|
53
108
|
// One registration covers every emitter of the type (bash uses it too).
|
|
54
|
-
for (const customType of ["task-notification", "wakeup"]) {
|
|
109
|
+
for (const customType of ["task-notification", "wakeup", "loop"]) {
|
|
55
110
|
pi.registerMessageRenderer(customType, (message, { expanded }, theme) =>
|
|
56
111
|
notificationComponent(theme, customMessageText(message.content), expanded),
|
|
57
112
|
);
|
|
58
113
|
}
|
|
59
114
|
|
|
60
|
-
const notify = (
|
|
61
|
-
pi.sendMessage(
|
|
62
|
-
{ customType, content: [{ type: "text", text }], display: true, details },
|
|
63
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
64
|
-
);
|
|
65
|
-
};
|
|
115
|
+
const notify = createTaskNotifier(pi);
|
|
66
116
|
|
|
67
117
|
pi.registerTool({
|
|
68
118
|
name: "monitor",
|
|
69
119
|
label: "Monitor",
|
|
70
120
|
...ccToolRenderers("Monitor"),
|
|
71
121
|
description:
|
|
72
|
-
"Start a background monitor that streams events from a long-running command. Each stdout line becomes a notification delivered to the conversation while you keep working; the command exiting ends the watch. Use for 'tell me every time X happens' (tail -f,
|
|
122
|
+
"Start a background monitor that streams events from a long-running command. Each stdout line becomes a notification delivered to the conversation while you keep working; the command exiting ends the watch. Use for 'tell me every time X happens' (tail -f, an until-loop that polls a condition); for a single command's completion prefer bash with run_in_background: true — its completion notification arrives on its own. Returns a task id — stop with task_stop, inspect with task_output (both deferred — load them with tool_search). Alternatively pass `ws` to watch a WebSocket (each text frame is an event).",
|
|
73
123
|
parameters: Type.Object({
|
|
74
124
|
command: Type.Optional(Type.String({ description: "Shell command; each stdout line is an event, exit ends the watch" })),
|
|
75
125
|
description: Type.String({ description: "Short description of what is being monitored (shown in notifications)" }),
|
|
@@ -87,7 +137,7 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
87
137
|
),
|
|
88
138
|
),
|
|
89
139
|
}),
|
|
90
|
-
async execute(_toolCallId, params,
|
|
140
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
91
141
|
lastCtx = ctx;
|
|
92
142
|
if (Boolean(params.command) === Boolean(params.ws)) {
|
|
93
143
|
return {
|
|
@@ -97,11 +147,18 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
97
147
|
};
|
|
98
148
|
}
|
|
99
149
|
|
|
150
|
+
// One-shot modes (`-p` / `--mode json`) exit when the turn settles: the
|
|
151
|
+
// notifier goes inert, shutdown kills the monitor, and nothing it saw
|
|
152
|
+
// reaches anyone — the tool result had promised "events arrive as
|
|
153
|
+
// notifications" (LIFECYCLE-REVIEW M3, measured). Run it to its end
|
|
154
|
+
// there (exit or deadline) and return the collected events in the
|
|
155
|
+
// result — the shape bash and Agent use in these modes.
|
|
156
|
+
const oneShot = !sessionOutlivesTurn(ctx.mode);
|
|
100
157
|
const id = generateTaskId();
|
|
101
158
|
let status: BackgroundTask["status"] = "running";
|
|
102
159
|
let stored = "";
|
|
103
160
|
let eventCount = 0;
|
|
104
|
-
let pending:
|
|
161
|
+
let pending: MonitorBatch = emptyBatch();
|
|
105
162
|
let flushTimer: NodeJS.Timeout | undefined;
|
|
106
163
|
let finish!: () => void;
|
|
107
164
|
const finished = new Promise<void>((resolve) => {
|
|
@@ -110,21 +167,24 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
110
167
|
|
|
111
168
|
const flush = () => {
|
|
112
169
|
flushTimer = undefined;
|
|
113
|
-
if (pending
|
|
170
|
+
if (batchSize(pending) === 0) return;
|
|
114
171
|
const batch = pending;
|
|
115
|
-
pending =
|
|
116
|
-
notify(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
);
|
|
172
|
+
pending = emptyBatch();
|
|
173
|
+
notify("task-notification", systemNotification(formatMonitorBatch(id, params.description, batch)), {
|
|
174
|
+
taskId: id,
|
|
175
|
+
events: batchSize(batch),
|
|
176
|
+
});
|
|
121
177
|
};
|
|
122
178
|
|
|
123
179
|
const onEvent = (line: string) => {
|
|
124
180
|
eventCount++;
|
|
125
181
|
stored = tail(`${stored}${line}\n`, STORED_OUTPUT_CAP);
|
|
126
|
-
|
|
127
|
-
|
|
182
|
+
if (oneShot) return; // collected into the tool result instead
|
|
183
|
+
pushEvent(pending, line);
|
|
184
|
+
// Bounded batches, and a wider window mid-turn so a chatty stream
|
|
185
|
+
// coalesces instead of steering one notification per second
|
|
186
|
+
// (monitor-batch.ts).
|
|
187
|
+
flushTimer ??= setTimeout(flush, agentBusy ? MONITOR_BATCH_BUSY_MS : MONITOR_BATCH_IDLE_MS);
|
|
128
188
|
};
|
|
129
189
|
|
|
130
190
|
const end = (finalStatus: BackgroundTask["status"], note?: string) => {
|
|
@@ -133,8 +193,12 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
133
193
|
task.status = finalStatus;
|
|
134
194
|
task.finishedAt = Date.now();
|
|
135
195
|
if (flushTimer) clearTimeout(flushTimer);
|
|
136
|
-
flush();
|
|
137
196
|
finish();
|
|
197
|
+
// Past this point everything touches the session: a monitor ending
|
|
198
|
+
// after shutdown (H1) or inside a one-shot run reports through
|
|
199
|
+
// `finished` alone.
|
|
200
|
+
if (!alive() || oneShot) return;
|
|
201
|
+
flush();
|
|
138
202
|
updateWidget();
|
|
139
203
|
notify(
|
|
140
204
|
"task-notification",
|
|
@@ -146,8 +210,13 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
146
210
|
let stopRequested = false;
|
|
147
211
|
let stop: () => void;
|
|
148
212
|
if (params.command) {
|
|
213
|
+
// Own process group: stop() must end the COMMAND, not just the `$SHELL -c`
|
|
214
|
+
// leader — with zsh a `cmd; echo` or a pipeline otherwise lives on as an
|
|
215
|
+
// orphan holding the stdout pipe, so the task never finishes and, in a
|
|
216
|
+
// one-shot run, the process cannot exit (LIFECYCLE-REVIEW M2, measured).
|
|
149
217
|
const child = spawn(process.env.SHELL || "/bin/sh", ["-c", params.command], {
|
|
150
218
|
cwd: ctx.cwd,
|
|
219
|
+
...detachedSpawnOptions(),
|
|
151
220
|
stdio: ["ignore", "pipe", "pipe"],
|
|
152
221
|
});
|
|
153
222
|
let buffer = "";
|
|
@@ -172,7 +241,7 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
172
241
|
);
|
|
173
242
|
stop = () => {
|
|
174
243
|
stopRequested = true;
|
|
175
|
-
child
|
|
244
|
+
stopProcessTree(child, KILL_GRACE_MS);
|
|
176
245
|
};
|
|
177
246
|
} else {
|
|
178
247
|
let ws: WebSocket;
|
|
@@ -211,17 +280,50 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
211
280
|
stop,
|
|
212
281
|
finished,
|
|
213
282
|
};
|
|
214
|
-
|
|
215
|
-
|
|
283
|
+
// A one-shot monitor is awaited below and never addressable afterwards, so
|
|
284
|
+
// it is not registered behind task_output/task_stop (like bash's blocking
|
|
285
|
+
// path); `persistent` cannot be honoured either — the deadline applies.
|
|
286
|
+
if (!oneShot) {
|
|
287
|
+
registry.register(task);
|
|
288
|
+
updateWidget();
|
|
289
|
+
}
|
|
216
290
|
|
|
217
|
-
|
|
218
|
-
|
|
291
|
+
const timeoutMs = Math.min(params.timeout_ms ?? DEFAULT_MONITOR_TIMEOUT_MS, MAX_MONITOR_TIMEOUT_MS);
|
|
292
|
+
if (oneShot || !params.persistent) {
|
|
219
293
|
const timer = setTimeout(() => {
|
|
220
294
|
if (task.status === "running") stop();
|
|
221
295
|
}, timeoutMs);
|
|
222
296
|
timer.unref?.();
|
|
223
297
|
}
|
|
224
298
|
|
|
299
|
+
if (oneShot) {
|
|
300
|
+
const unhook = whenAborted(signal, stop);
|
|
301
|
+
try {
|
|
302
|
+
await finished;
|
|
303
|
+
} finally {
|
|
304
|
+
unhook();
|
|
305
|
+
}
|
|
306
|
+
const output = persistIfLarge(stored.trim() || "(no events)", { dir: sessionResultsDir(ctx), id: `monitor-${id}` });
|
|
307
|
+
const finalStatus = task.status;
|
|
308
|
+
let how: string = finalStatus;
|
|
309
|
+
if (finalStatus === "stopped") {
|
|
310
|
+
if (signal?.aborted) how = "stopped (tool call aborted)";
|
|
311
|
+
else if (params.persistent) how = `stopped at its default deadline of ${timeoutMs} ms (persistent is not available in a one-shot session)`;
|
|
312
|
+
else how = `stopped at its ${timeoutMs} ms deadline`;
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
content: [
|
|
316
|
+
{
|
|
317
|
+
type: "text",
|
|
318
|
+
text: `Monitor ${id} (${params.description}) ${how} after ${eventCount} event(s). ${oneShotNote("monitor")}\n\n${output}`,
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
// No taskId: nothing is registered behind task_output/task_stop.
|
|
322
|
+
details: { status: finalStatus, events: eventCount },
|
|
323
|
+
isError: finalStatus === "failed",
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
225
327
|
return {
|
|
226
328
|
content: [
|
|
227
329
|
{
|
|
@@ -239,7 +341,7 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
239
341
|
label: "Task Output",
|
|
240
342
|
...ccToolRenderers("Task Output"),
|
|
241
343
|
description:
|
|
242
|
-
"Retrieve output from a running or finished background task (monitor, background subagent, or background bash) by task id. block=true (default) waits up to `timeout` ms for completion; block=false returns the current status immediately. You never need this just to learn that a task finished — completion
|
|
344
|
+
"Retrieve output from a running or finished background task (monitor, background subagent, or background bash) by task id. block=true (default) waits up to `timeout` ms for completion; block=false returns the current status immediately. You never need this just to learn that a task finished — completion arrives as a system notification with the status and the last 2 KB of output; call this for the rest of the output, when your next step needs the result now, or for a mid-run peek.",
|
|
243
345
|
parameters: Type.Object({
|
|
244
346
|
task_id: Type.String({ description: "The task id to get output from" }),
|
|
245
347
|
block: Type.Optional(Type.Boolean({ description: "Wait for completion (default true)" })),
|
|
@@ -328,11 +430,12 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
328
430
|
title: (a) => (a?.stop ? "stop" : a?.delaySeconds !== undefined ? `${a.delaySeconds}s` : undefined),
|
|
329
431
|
}),
|
|
330
432
|
description:
|
|
331
|
-
"Schedule when to resume work on a self-paced recurring task
|
|
433
|
+
"Schedule when to resume work on a self-paced recurring task — the dynamic mode of the /loop command. After `delaySeconds` (clamped to [60, 3600]) the given prompt is delivered as a system notification and a new turn starts. One wakeup is pending at a time — scheduling again replaces it; {stop: true} ends the loop and cancels any pending wakeup.\n\nPass the same task back via `prompt` each turn so the next firing repeats it. Set `noop: true` when nothing changed this tick (you checked and there's nothing to report); `noop: false` when something happened worth keeping. Pick `delaySeconds` from what you're actually waiting for: poll external state (a CI run, a deploy) at the rate it changes; for a quiet idle heartbeat prefer a long delay (1200s+). Do NOT schedule a short wakeup just to poll background work you started here — its completion already notifies you.",
|
|
332
434
|
parameters: Type.Object({
|
|
333
435
|
delaySeconds: Type.Optional(Type.Number({ description: "Seconds from now to wake up, clamped to [60, 3600]. Required unless stop is true" })),
|
|
334
436
|
prompt: Type.Optional(Type.String({ description: "The task to continue when the wakeup fires. Required unless stop is true" })),
|
|
335
437
|
reason: Type.Optional(Type.String({ description: "One short sentence explaining the chosen delay; shown to the user. Required unless stop is true" })),
|
|
438
|
+
noop: Type.Optional(Type.Boolean({ description: "true when nothing changed this tick (quiet hold); false when something happened worth keeping" })),
|
|
336
439
|
stop: Type.Optional(Type.Boolean({ description: "End the loop: cancel any pending wakeup and schedule nothing" })),
|
|
337
440
|
}),
|
|
338
441
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -364,15 +467,15 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
364
467
|
// Valid reschedule — now it is safe to replace any pending wakeup.
|
|
365
468
|
clearPending();
|
|
366
469
|
|
|
367
|
-
const request = { delaySeconds: params.delaySeconds, prompt: params.prompt, reason: params.reason };
|
|
470
|
+
const request = { delaySeconds: params.delaySeconds, prompt: params.prompt, reason: params.reason, noop: params.noop };
|
|
368
471
|
const delayMs = clampDelaySeconds(params.delaySeconds) * 1000;
|
|
369
472
|
const timer = setTimeout(() => {
|
|
370
473
|
wakeup = undefined;
|
|
371
|
-
notify("wakeup", buildWakeupMessage(request), { reason: request.reason });
|
|
474
|
+
notify("wakeup", buildWakeupMessage(request), { reason: request.reason, noop: request.noop ?? false });
|
|
372
475
|
}, delayMs);
|
|
373
476
|
timer.unref?.();
|
|
374
477
|
wakeup = { timer, prompt: params.prompt, reason: params.reason };
|
|
375
|
-
return { content: [{ type: "text", text: describeSchedule(request) }], details: { delayMs } };
|
|
478
|
+
return { content: [{ type: "text", text: describeSchedule(request) }], details: { delayMs, noop: params.noop ?? false } };
|
|
376
479
|
},
|
|
377
480
|
});
|
|
378
481
|
|
|
@@ -393,13 +496,123 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
393
496
|
},
|
|
394
497
|
});
|
|
395
498
|
|
|
499
|
+
const clearLoop = () => {
|
|
500
|
+
if (loop) {
|
|
501
|
+
clearInterval(loop.timer);
|
|
502
|
+
loop = undefined;
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
const clearWakeup = () => {
|
|
506
|
+
if (wakeup) {
|
|
507
|
+
clearTimeout(wakeup.timer);
|
|
508
|
+
wakeup = undefined;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
pi.registerCommand("loop", {
|
|
513
|
+
description: "Repeat a task on a loop: `/loop 5m <task>` (fixed interval) or `/loop <task>` (self-paced). `/loop stop` ends it.",
|
|
514
|
+
handler: async (args, ctx) => {
|
|
515
|
+
lastCtx = ctx;
|
|
516
|
+
const raw = (args ?? "").trim();
|
|
517
|
+
const keyword = raw.toLowerCase();
|
|
518
|
+
|
|
519
|
+
if (keyword === "stop") {
|
|
520
|
+
const had = Boolean(loop || wakeup);
|
|
521
|
+
clearLoop();
|
|
522
|
+
clearWakeup();
|
|
523
|
+
if (activatedWakeupTool) {
|
|
524
|
+
const active = pi.getActiveTools();
|
|
525
|
+
if (active.includes("schedule_wakeup")) pi.setActiveTools(active.filter((t) => t !== "schedule_wakeup"));
|
|
526
|
+
activatedWakeupTool = false;
|
|
527
|
+
}
|
|
528
|
+
ctx.ui.notify(had ? "Loop stopped; no further iterations will fire." : "No loop was running.", "info");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (raw === "" || keyword === "status") {
|
|
532
|
+
const parts: string[] = [];
|
|
533
|
+
if (loop) parts.push(`fixed interval every ${loop.intervalSeconds}s — ${loop.task}`);
|
|
534
|
+
if (wakeup) parts.push(`self-paced wakeup pending — ${wakeup.reason}`);
|
|
535
|
+
ctx.ui.notify(
|
|
536
|
+
parts.length
|
|
537
|
+
? `Running:\n- ${parts.join("\n- ")}\nStop with /loop stop.`
|
|
538
|
+
: "No loop running. Start with `/loop [interval] <task>`, e.g. `/loop 5m check the build`.",
|
|
539
|
+
"info",
|
|
540
|
+
);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const parsed = parseLoopArgs(raw);
|
|
545
|
+
if (!parsed.task) {
|
|
546
|
+
ctx.ui.notify("Give a task to loop, e.g. `/loop 10m check for new PRs` or `/loop watch the deploy`.", "info");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// One loop at a time — replace any existing interval loop or pending wakeup.
|
|
551
|
+
clearLoop();
|
|
552
|
+
clearWakeup();
|
|
553
|
+
|
|
554
|
+
if (parsed.intervalSeconds !== undefined) {
|
|
555
|
+
// Fixed interval, harness-driven: re-arm automatically and fire the first tick now.
|
|
556
|
+
const seconds = clampDelaySeconds(parsed.intervalSeconds);
|
|
557
|
+
const adjusted =
|
|
558
|
+
seconds !== parsed.intervalSeconds
|
|
559
|
+
? ` (adjusted from ${parsed.intervalSeconds}s; allowed range ${MIN_DELAY_SECONDS}-${MAX_DELAY_SECONDS}s)`
|
|
560
|
+
: "";
|
|
561
|
+
// Skip (don't queue) a tick while the previous tick's turn is still
|
|
562
|
+
// running, so a slow task can't build a backlog of stale iterations.
|
|
563
|
+
const tick = () => {
|
|
564
|
+
if (agentBusy) return;
|
|
565
|
+
notify("loop", buildLoopMessage(parsed.task), { intervalSeconds: seconds });
|
|
566
|
+
};
|
|
567
|
+
const timer = setInterval(tick, seconds * 1000);
|
|
568
|
+
timer.unref?.();
|
|
569
|
+
loop = { timer, intervalSeconds: seconds, task: parsed.task };
|
|
570
|
+
ctx.ui.notify(`Looping every ${seconds}s${adjusted}: ${parsed.task}. Stop with /loop stop.`, "info");
|
|
571
|
+
tick(); // fire the first iteration now
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Dynamic (self-paced): the model drives schedule_wakeup, so make sure it is
|
|
576
|
+
// active — remembering we did, so /loop stop can restore the lean surface.
|
|
577
|
+
const active = pi.getActiveTools();
|
|
578
|
+
if (!active.includes("schedule_wakeup")) {
|
|
579
|
+
pi.setActiveTools([...active, "schedule_wakeup"]);
|
|
580
|
+
activatedWakeupTool = true;
|
|
581
|
+
}
|
|
582
|
+
ctx.ui.notify(`Self-paced loop started: ${parsed.task}. Stop with /loop stop.`, "info");
|
|
583
|
+
notify("loop", buildDynamicLoopPrompt(parsed.task), {});
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
|
|
396
587
|
pi.on("session_start", (_event, ctx) => {
|
|
397
588
|
lastCtx = ctx;
|
|
589
|
+
if (pendingShutdownNotice) {
|
|
590
|
+
const notice = pendingShutdownNotice;
|
|
591
|
+
pendingShutdownNotice = undefined;
|
|
592
|
+
if (ctx.hasUI) ctx.ui.notify(notice, "info");
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
pi.on("agent_start", () => {
|
|
596
|
+
agentBusy = true;
|
|
597
|
+
});
|
|
598
|
+
pi.on("agent_settled", () => {
|
|
599
|
+
agentBusy = false;
|
|
398
600
|
});
|
|
399
601
|
|
|
400
|
-
pi.on("session_shutdown", () => {
|
|
602
|
+
pi.on("session_shutdown", (event) => {
|
|
603
|
+
// Fires on /clear, /new, /resume and /reload too (findings §8). Everything
|
|
604
|
+
// running dies with this instance — the replacement instance has no handle
|
|
605
|
+
// on it — and the replacement is told (see pendingShutdownNotice); a quit
|
|
606
|
+
// needs no note. A reload keeps the conversation, so its note says so.
|
|
607
|
+
const running = registry.running();
|
|
608
|
+
if (running.length > 0 && event.reason !== "quit") {
|
|
609
|
+
const names = running.map((t) => `${t.id} (${t.description})`).join(", ");
|
|
610
|
+
const when = event.reason === "reload" ? "on reload" : "with the previous session";
|
|
611
|
+
pendingShutdownNotice = `Stopped ${running.length} background task${running.length === 1 ? "" : "s"} ${when}: ${names}.`;
|
|
612
|
+
}
|
|
401
613
|
registry.stopAll();
|
|
402
|
-
|
|
403
|
-
|
|
614
|
+
clearWakeup();
|
|
615
|
+
clearLoop();
|
|
616
|
+
lastCtx = undefined;
|
|
404
617
|
});
|
|
405
618
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitor event batching (pure). A monitor turns every stdout line into a
|
|
3
|
+
* steered notification; `tail -f` on a busy log used to mean an unbounded
|
|
4
|
+
* batch every second, each one a turn when the session was idle. Two limits:
|
|
5
|
+
*
|
|
6
|
+
* - a batch shows at most MAX_LINES lines / MAX_CHARS characters and says how
|
|
7
|
+
* many more arrived (the full stream stays behind `task_output`);
|
|
8
|
+
* - while the agent is mid-turn the flush window widens (BUSY_MS), so events
|
|
9
|
+
* coalesce into one notification at the next tool boundary instead of one
|
|
10
|
+
* steer per second. Idle, the short window keeps latency low; the turn it
|
|
11
|
+
* starts then widens the window for what follows.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const MONITOR_BATCH_MAX_LINES = 50;
|
|
15
|
+
export const MONITOR_BATCH_MAX_CHARS = 4_000;
|
|
16
|
+
export const MONITOR_BATCH_IDLE_MS = 1_000;
|
|
17
|
+
export const MONITOR_BATCH_BUSY_MS = 10_000;
|
|
18
|
+
|
|
19
|
+
export interface MonitorBatch {
|
|
20
|
+
/** Lines kept for display, in order (at most MAX_LINES). */
|
|
21
|
+
shown: string[];
|
|
22
|
+
/** Lines that arrived beyond the cap; counted, not kept. */
|
|
23
|
+
overflow: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function emptyBatch(): MonitorBatch {
|
|
27
|
+
return { shown: [], overflow: 0 };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Add one event line, keeping the batch bounded. */
|
|
31
|
+
export function pushEvent(batch: MonitorBatch, line: string): void {
|
|
32
|
+
if (batch.shown.length < MONITOR_BATCH_MAX_LINES) batch.shown.push(line);
|
|
33
|
+
else batch.overflow++;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function batchSize(batch: MonitorBatch): number {
|
|
37
|
+
return batch.shown.length + batch.overflow;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The notification body for one flushed batch. */
|
|
41
|
+
export function formatMonitorBatch(id: string, description: string, batch: MonitorBatch): string {
|
|
42
|
+
const total = batchSize(batch);
|
|
43
|
+
const lines: string[] = [];
|
|
44
|
+
let chars = 0;
|
|
45
|
+
let hidden = batch.overflow;
|
|
46
|
+
for (const line of batch.shown) {
|
|
47
|
+
if (chars + line.length + 1 > MONITOR_BATCH_MAX_CHARS && lines.length > 0) {
|
|
48
|
+
hidden += batch.shown.length - lines.length;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
const shown = line.length > MONITOR_BATCH_MAX_CHARS ? `${line.slice(0, MONITOR_BATCH_MAX_CHARS)}…` : line;
|
|
52
|
+
lines.push(shown);
|
|
53
|
+
chars += shown.length + 1;
|
|
54
|
+
}
|
|
55
|
+
const head = `Monitor ${id} (${description}) emitted ${total} event(s):`;
|
|
56
|
+
const more = hidden > 0 ? `\n… +${hidden} more line(s) not shown — task_output ${id} has the full stream` : "";
|
|
57
|
+
return `${head}\n${lines.join("\n")}${more}`;
|
|
58
|
+
}
|
|
@@ -20,6 +20,8 @@ export interface BackgroundTask {
|
|
|
20
20
|
status: BackgroundStatus;
|
|
21
21
|
startedAt: number;
|
|
22
22
|
finishedAt?: number;
|
|
23
|
+
/** The raw shell command, when the task wraps one (shown by the shell panel). */
|
|
24
|
+
command?: string;
|
|
23
25
|
/** File the task spools its output to, when it has one. */
|
|
24
26
|
logPath?: string;
|
|
25
27
|
/** Output accumulated so far (or final output once finished). */
|
|
@@ -33,6 +35,13 @@ export interface BackgroundTask {
|
|
|
33
35
|
* terminate it even though the task itself already completed.
|
|
34
36
|
*/
|
|
35
37
|
resident?: () => boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The task already has dedicated panel UI showing it live (bash shells and
|
|
40
|
+
* subagent runs render in the subagents panel), so ambient counters like
|
|
41
|
+
* the " background tasks: N" widget must not double-count it. Set by the
|
|
42
|
+
* producer — never re-derived from `kind` by consumers.
|
|
43
|
+
*/
|
|
44
|
+
ownUI?: boolean;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
export function generateTaskId(): string {
|
|
@@ -16,6 +16,51 @@ export interface WakeupRequest {
|
|
|
16
16
|
delaySeconds: number;
|
|
17
17
|
prompt: string;
|
|
18
18
|
reason: string;
|
|
19
|
+
/** Nothing changed this tick (matches CC's /loop noop flag). Recorded for callers; no effect on scheduling. */
|
|
20
|
+
noop?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ParsedLoop {
|
|
24
|
+
/** undefined = dynamic (model self-paces via schedule_wakeup); set = fixed interval. */
|
|
25
|
+
intervalSeconds?: number;
|
|
26
|
+
task: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse `/loop` arguments: a leading duration token (`5m`, `30s`, `2h`) selects
|
|
31
|
+
* fixed-interval mode and the rest is the task; no duration selects dynamic
|
|
32
|
+
* (self-paced) mode. `stop` / `status` are handled by the caller, not here.
|
|
33
|
+
*/
|
|
34
|
+
export function parseLoopArgs(raw: string): ParsedLoop {
|
|
35
|
+
const m = raw.trim().match(/^(\d+)\s*(s|m|h)\b\s*([\s\S]*)$/i);
|
|
36
|
+
if (m) {
|
|
37
|
+
const n = Number(m[1]);
|
|
38
|
+
const unit = m[2].toLowerCase();
|
|
39
|
+
const seconds = unit === "h" ? n * 3600 : unit === "m" ? n * 60 : n;
|
|
40
|
+
return { intervalSeconds: seconds, task: m[3].trim() };
|
|
41
|
+
}
|
|
42
|
+
return { task: raw.trim() };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A loop follow-up: an instruction line, a blank, then the task — system-framed. */
|
|
46
|
+
function framedLoop(lead: string, task: string): string {
|
|
47
|
+
return systemNotification([lead, "", task].join("\n"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The follow-up a fixed-interval `/loop` tick delivers. */
|
|
51
|
+
export function buildLoopMessage(task: string): string {
|
|
52
|
+
return framedLoop(
|
|
53
|
+
"Loop tick — run the task below now. This repeats automatically on the interval and keeps firing until the user runs `/loop stop`.",
|
|
54
|
+
task,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The opening message for a dynamic (self-paced) `/loop` — instructs the model to drive schedule_wakeup. */
|
|
59
|
+
export function buildDynamicLoopPrompt(task: string): string {
|
|
60
|
+
return framedLoop(
|
|
61
|
+
"Self-paced loop started. Work the task below now. When this iteration is done, call schedule_wakeup to schedule the next one — choose delaySeconds by what you're waiting for, and pass the same task back as `prompt`. End the loop with schedule_wakeup {stop: true} when the task is complete or the user says to stop.",
|
|
62
|
+
task,
|
|
63
|
+
);
|
|
19
64
|
}
|
|
20
65
|
|
|
21
66
|
/** The follow-up message a fired wakeup delivers. Framed so it never reads as user input. */
|