agent-tempo 1.2.0 → 1.4.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/CLAUDE.md +253 -219
- package/LICENSE +21 -21
- package/README.md +293 -289
- package/assets/icon-dark.svg +9 -9
- package/assets/icon.svg +9 -9
- package/assets/logo-dark.svg +11 -11
- package/assets/logo-light.svg +11 -11
- package/dashboard/README.md +91 -91
- package/dashboard/dist/assets/{index-D6Xyje_n.js → index-jmYe6rmS.js} +2 -2
- package/dashboard/dist/assets/index-jmYe6rmS.js.map +1 -0
- package/dashboard/dist/index.html +20 -20
- package/dashboard/package.json +47 -47
- package/dist/activities/outbox.d.ts +30 -1
- package/dist/activities/outbox.js +96 -3
- package/dist/adapters/base.js +5 -0
- package/dist/adapters/copilot/adapter.js +12 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +7 -0
- package/dist/adapters/pi/adapter.d.ts +2 -0
- package/dist/adapters/pi/adapter.js +43 -0
- package/dist/adapters/pi/index.d.ts +16 -0
- package/dist/adapters/pi/index.js +10 -0
- package/dist/cli/global-wrapper.d.ts +19 -0
- package/dist/cli/global-wrapper.js +169 -0
- package/dist/cli/help-text.js +97 -97
- package/dist/cli/startup.js +11 -0
- package/dist/cli/upgrade-command.js +81 -81
- package/dist/cli.js +12 -0
- package/dist/client/core.js +9 -2
- package/dist/client/interface.d.ts +6 -0
- package/dist/config.d.ts +79 -0
- package/dist/config.js +74 -0
- package/dist/daemon.js +37 -1
- package/dist/http/aggregate.d.ts +22 -1
- package/dist/http/aggregate.js +41 -0
- package/dist/http/auth.d.ts +94 -8
- package/dist/http/auth.js +93 -9
- package/dist/http/body.d.ts +4 -1
- package/dist/http/body.js +6 -3
- package/dist/http/event-bus.js +1 -0
- package/dist/http/event-types.d.ts +34 -2
- package/dist/http/event-types.js +1 -0
- package/dist/http/gate-audit.d.ts +12 -0
- package/dist/http/gate-audit.js +95 -0
- package/dist/http/gate-registry.d.ts +167 -0
- package/dist/http/gate-registry.js +163 -0
- package/dist/http/gate-routes.d.ts +48 -0
- package/dist/http/gate-routes.js +102 -0
- package/dist/http/ingest-registry.d.ts +30 -0
- package/dist/http/ingest-registry.js +108 -0
- package/dist/http/inner-loop-routes.d.ts +66 -0
- package/dist/http/inner-loop-routes.js +182 -0
- package/dist/http/inner-loop.d.ts +92 -0
- package/dist/http/inner-loop.js +155 -0
- package/dist/http/server.d.ts +38 -3
- package/dist/http/server.js +211 -6
- package/dist/http/snapshot.d.ts +6 -0
- package/dist/http/snapshot.js +6 -0
- package/dist/pi/cue-pump.d.ts +61 -0
- package/dist/pi/cue-pump.js +95 -0
- package/dist/pi/extension.d.ts +45 -0
- package/dist/pi/extension.js +407 -0
- package/dist/pi/gate-client.d.ts +54 -0
- package/dist/pi/gate-client.js +136 -0
- package/dist/pi/headless.d.ts +85 -0
- package/dist/pi/headless.js +224 -0
- package/dist/pi/index.d.ts +28 -0
- package/dist/pi/index.js +43 -0
- package/dist/pi/inner-loop-client.d.ts +67 -0
- package/dist/pi/inner-loop-client.js +164 -0
- package/dist/pi/inner-loop-publisher.d.ts +187 -0
- package/dist/pi/inner-loop-publisher.js +236 -0
- package/dist/pi/lazy-proxy.d.ts +37 -0
- package/dist/pi/lazy-proxy.js +55 -0
- package/dist/pi/mission-control/actions.d.ts +48 -0
- package/dist/pi/mission-control/actions.js +98 -0
- package/dist/pi/mission-control/board.d.ts +53 -0
- package/dist/pi/mission-control/board.js +104 -0
- package/dist/pi/mission-control/extension.d.ts +44 -0
- package/dist/pi/mission-control/extension.js +251 -0
- package/dist/pi/mission-control/index.d.ts +15 -0
- package/dist/pi/mission-control/index.js +32 -0
- package/dist/pi/mission-control/inner-tail.d.ts +48 -0
- package/dist/pi/mission-control/inner-tail.js +76 -0
- package/dist/pi/mission-control/pi-ui.d.ts +43 -0
- package/dist/pi/mission-control/pi-ui.js +10 -0
- package/dist/pi/mission-control/render.d.ts +6 -0
- package/dist/pi/mission-control/render.js +95 -0
- package/dist/pi/phase-driver.d.ts +74 -0
- package/dist/pi/phase-driver.js +122 -0
- package/dist/pi/pi-types.d.ts +208 -0
- package/dist/pi/pi-types.js +21 -0
- package/dist/pi/probe.d.ts +80 -0
- package/dist/pi/probe.js +154 -0
- package/dist/pi/render-tools.d.ts +17 -0
- package/dist/pi/render-tools.js +51 -0
- package/dist/pi/reset-pump.d.ts +47 -0
- package/dist/pi/reset-pump.js +85 -0
- package/dist/pi/tool-capability.d.ts +60 -0
- package/dist/pi/tool-capability.js +156 -0
- package/dist/pi/workflow-client.d.ts +158 -0
- package/dist/pi/workflow-client.js +289 -0
- package/dist/pi/zod-to-typebox.d.ts +74 -0
- package/dist/pi/zod-to-typebox.js +191 -0
- package/dist/scripts/verify-daemon-isolation-guard.js +24 -24
- package/dist/server-tools.d.ts +2 -0
- package/dist/server-tools.js +50 -46
- package/dist/server.js +4 -0
- package/dist/spawn.d.ts +55 -0
- package/dist/spawn.js +84 -12
- package/dist/tools/agent-types.d.ts +2 -2
- package/dist/tools/agent-types.js +22 -17
- package/dist/tools/attachment-info.d.ts +2 -2
- package/dist/tools/attachment-info.js +38 -33
- package/dist/tools/broadcast.d.ts +2 -2
- package/dist/tools/broadcast.js +69 -64
- package/dist/tools/cancel-stage.d.ts +2 -2
- package/dist/tools/cancel-stage.js +20 -15
- package/dist/tools/clear-state.d.ts +2 -2
- package/dist/tools/clear-state.js +25 -20
- package/dist/tools/coat-check-evict.d.ts +2 -2
- package/dist/tools/coat-check-evict.js +30 -25
- package/dist/tools/coat-check-get.d.ts +2 -2
- package/dist/tools/coat-check-get.js +39 -34
- package/dist/tools/coat-check-list.d.ts +2 -2
- package/dist/tools/coat-check-list.js +48 -43
- package/dist/tools/coat-check-put.d.ts +2 -2
- package/dist/tools/coat-check-put.js +41 -36
- package/dist/tools/cue.d.ts +2 -2
- package/dist/tools/cue.js +57 -52
- package/dist/tools/descriptor.d.ts +72 -0
- package/dist/tools/descriptor.js +39 -0
- package/dist/tools/destroy.d.ts +2 -2
- package/dist/tools/destroy.js +153 -148
- package/dist/tools/ensemble.d.ts +2 -2
- package/dist/tools/ensemble.js +71 -66
- package/dist/tools/evaluate-gate.d.ts +2 -2
- package/dist/tools/evaluate-gate.js +33 -27
- package/dist/tools/fetch-state.d.ts +2 -2
- package/dist/tools/fetch-state.js +43 -38
- package/dist/tools/gates.d.ts +2 -2
- package/dist/tools/gates.js +39 -34
- package/dist/tools/hosts.d.ts +2 -2
- package/dist/tools/hosts.js +25 -20
- package/dist/tools/listen.d.ts +2 -2
- package/dist/tools/listen.js +23 -18
- package/dist/tools/load-lineup.d.ts +2 -2
- package/dist/tools/load-lineup.js +324 -319
- package/dist/tools/migrate.d.ts +2 -2
- package/dist/tools/migrate.js +45 -40
- package/dist/tools/pause.d.ts +2 -2
- package/dist/tools/pause.js +34 -29
- package/dist/tools/play.d.ts +2 -2
- package/dist/tools/play.js +53 -48
- package/dist/tools/quality-gate.d.ts +2 -2
- package/dist/tools/quality-gate.js +26 -21
- package/dist/tools/recall.d.ts +2 -2
- package/dist/tools/recall.js +32 -27
- package/dist/tools/recruit.d.ts +2 -2
- package/dist/tools/recruit.js +325 -256
- package/dist/tools/release.d.ts +2 -2
- package/dist/tools/release.js +85 -80
- package/dist/tools/report.d.ts +2 -2
- package/dist/tools/report.js +28 -23
- package/dist/tools/reset.d.ts +3 -0
- package/dist/tools/reset.js +51 -0
- package/dist/tools/restart.d.ts +2 -2
- package/dist/tools/restart.js +51 -46
- package/dist/tools/restore.d.ts +2 -2
- package/dist/tools/restore.js +76 -71
- package/dist/tools/save-lineup.d.ts +2 -2
- package/dist/tools/save-lineup.js +32 -27
- package/dist/tools/save-state.d.ts +2 -2
- package/dist/tools/save-state.js +43 -38
- package/dist/tools/schedule.d.ts +2 -2
- package/dist/tools/schedule.js +133 -128
- package/dist/tools/schedules.d.ts +2 -2
- package/dist/tools/schedules.js +41 -36
- package/dist/tools/set-ensemble-description.d.ts +2 -2
- package/dist/tools/set-ensemble-description.js +26 -21
- package/dist/tools/set-name.d.ts +2 -2
- package/dist/tools/set-name.js +38 -33
- package/dist/tools/set-part.d.ts +2 -2
- package/dist/tools/set-part.js +20 -15
- package/dist/tools/shutdown.d.ts +2 -2
- package/dist/tools/shutdown.js +39 -34
- package/dist/tools/stage.d.ts +2 -2
- package/dist/tools/stage.js +28 -23
- package/dist/tools/stages.d.ts +2 -2
- package/dist/tools/stages.js +36 -31
- package/dist/tools/unschedule.d.ts +2 -2
- package/dist/tools/unschedule.js +30 -25
- package/dist/tools/who-am-i.d.ts +2 -2
- package/dist/tools/who-am-i.js +36 -31
- package/dist/tools/worktree.d.ts +2 -2
- package/dist/tools/worktree.js +134 -129
- package/dist/tui/index.js +6 -6
- package/dist/types.d.ts +47 -2
- package/dist/types.js +1 -1
- package/dist/utils/default-part.js +1 -0
- package/dist/utils/grpc-shutdown-guard.d.ts +52 -0
- package/dist/utils/grpc-shutdown-guard.js +88 -0
- package/dist/utils/sdk-probe.d.ts +23 -0
- package/dist/utils/sdk-probe.js +46 -7
- package/dist/worker.d.ts +3 -1
- package/dist/worker.js +6 -2
- package/dist/workflows/session.js +70 -2
- package/dist/workflows/signals.d.ts +32 -2
- package/dist/workflows/signals.js +25 -2
- package/examples/agents/tempo-composer.md +56 -56
- package/examples/agents/tempo-conductor.md +117 -117
- package/examples/agents/tempo-critic.md +73 -73
- package/examples/agents/tempo-improv.md +74 -74
- package/examples/agents/tempo-liner.md +75 -75
- package/examples/agents/tempo-roadie.md +61 -61
- package/examples/agents/tempo-soloist.md +71 -71
- package/examples/agents/tempo-tuner.md +94 -94
- package/examples/ensembles/tempo-big-band.yaml +146 -146
- package/examples/ensembles/tempo-dev-team.yaml +58 -58
- package/examples/ensembles/tempo-headless-jam.yaml +77 -77
- package/examples/ensembles/tempo-jam-session.yaml +41 -41
- package/examples/ensembles/tempo-mock-jam.yaml +79 -79
- package/examples/ensembles/tempo-review-squad.yaml +32 -32
- package/package.json +176 -173
- package/packaging/launchd/com.agent.tempo.plist +46 -46
- package/packaging/systemd/agent-tempo.service +32 -32
- package/packaging/windows/install-task.ps1 +71 -71
- package/scenarios/conductor-recruit-mock.yaml +33 -33
- package/scenarios/echo-roundtrip.yaml +15 -15
- package/scenarios/multi-player-handoff.yaml +38 -38
- package/scenarios/recruit-cascade.yaml +38 -38
- package/scenarios/two-player-conversation.yaml +33 -33
- package/workflow-bundle.js +97 -6
- package/dashboard/dist/assets/index-D6Xyje_n.js.map +0 -1
- package/dist/activities/claude-stop.d.ts +0 -21
- package/dist/activities/claude-stop.js +0 -94
- package/dist/channel.d.ts +0 -3
- package/dist/channel.js +0 -48
- package/dist/copilot-bridge.d.ts +0 -22
- package/dist/copilot-bridge.js +0 -565
- package/dist/scripts/258-spotcheck.js +0 -303
- package/dist/tools/detach.d.ts +0 -4
- package/dist/tools/detach.js +0 -45
- package/dist/tools/encore.d.ts +0 -4
- package/dist/tools/encore.js +0 -31
- package/dist/tools/helpers.d.ts +0 -21
- package/dist/tools/helpers.js +0 -25
- package/dist/tools/pause-ensemble.d.ts +0 -4
- package/dist/tools/pause-ensemble.js +0 -58
- package/dist/tools/resume-ensemble.d.ts +0 -4
- package/dist/tools/resume-ensemble.js +0 -79
- package/dist/tools/stop.d.ts +0 -4
- package/dist/tools/stop.js +0 -29
- package/dist/tui/client.d.ts +0 -6
- package/dist/tui/client.js +0 -9
- package/dist/tui/components/ActivityLog.d.ts +0 -16
- package/dist/tui/components/ActivityLog.js +0 -36
- package/dist/tui/components/CommandOverlay.d.ts +0 -15
- package/dist/tui/components/CommandOverlay.js +0 -34
- package/dist/tui/components/ConductorChat.d.ts +0 -16
- package/dist/tui/components/ConductorChat.js +0 -32
- package/dist/tui/components/EnsembleListView.d.ts +0 -14
- package/dist/tui/components/EnsembleListView.js +0 -32
- package/dist/tui/components/EnsemblePanel.d.ts +0 -12
- package/dist/tui/components/EnsemblePanel.js +0 -40
- package/dist/tui/components/InputBar.d.ts +0 -13
- package/dist/tui/components/InputBar.js +0 -58
- package/dist/tui/components/ScheduleOverlay.d.ts +0 -13
- package/dist/tui/components/ScheduleOverlay.js +0 -113
- package/dist/tui/components/TopBar.d.ts +0 -12
- package/dist/tui/components/TopBar.js +0 -15
- package/dist/tui/core-api.d.ts +0 -26
- package/dist/tui/core-api.js +0 -67
- package/dist/tui/hooks/useEnsembleDiscovery.d.ts +0 -3
- package/dist/tui/hooks/useEnsembleDiscovery.js +0 -30
- package/dist/tui/hooks/useMaestroPoller.d.ts +0 -3
- package/dist/tui/hooks/useMaestroPoller.js +0 -36
- package/dist/tui/hooks/useSendCommand.d.ts +0 -7
- package/dist/tui/hooks/useSendCommand.js +0 -29
- package/dist/utils/bg-preflight.d.ts +0 -25
- package/dist/utils/bg-preflight.js +0 -154
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool capability classifier (3d / MD-C) — a PURE function mapping a tool name
|
|
3
|
+
* to its risk class. The 3d operator gate and the MD-C headless tool-access
|
|
4
|
+
* posture consume it; this module owns the MECHANISM, tempo-security owns the
|
|
5
|
+
* CONTENT (the name-sets below) and signs off on it.
|
|
6
|
+
*
|
|
7
|
+
* - `'exec'` — shell / arbitrary-code execution. HARD-BLOCKED when the
|
|
8
|
+
* headless tool-access policy is `restricted` (MD-C). The
|
|
9
|
+
* highest-danger class.
|
|
10
|
+
* - `'high-blast'` — destructive or exfiltration-capable actions (file writes,
|
|
11
|
+
* network fetch, ensemble mutation). Allowed unsupervised,
|
|
12
|
+
* but routed to the OPERATOR GATE when an operator is armed.
|
|
13
|
+
* - `'low-risk'` — read-only / coordination. Always bypasses the gate.
|
|
14
|
+
*
|
|
15
|
+
* UNKNOWN tool names default to {@link UNKNOWN_DEFAULT} — a FAIL-SAFE choice
|
|
16
|
+
* (`'high-blast'`: never silently bypass a tool we don't recognize; gate it when
|
|
17
|
+
* an operator is armed). Whether an unknown tool should instead hard-block
|
|
18
|
+
* (`'exec'`) is a tempo-security posture call — see the cue to security.
|
|
19
|
+
*
|
|
20
|
+
* Matching is CASE-INSENSITIVE on the trimmed tool name. Pi's `tool_call`
|
|
21
|
+
* `toolName` is a bare string (built-ins like `bash`/`read`/`edit`/`write`/`grep`
|
|
22
|
+
* plus the natively-registered agent-tempo MCP tools), so we normalize before
|
|
23
|
+
* lookup.
|
|
24
|
+
*
|
|
25
|
+
* ⚠️ NAME-SET OWNERSHIP: the three Sets are tempo-security's posture content.
|
|
26
|
+
* They are a STARTER set (conductor's examples + Pi built-ins + the agent-tempo
|
|
27
|
+
* coordination/mutation tools) pending security's authoritative sign-off; amend
|
|
28
|
+
* the Set membership without touching the mechanism. The classifier behavior
|
|
29
|
+
* (and its tests) are keyed on representative names that won't move.
|
|
30
|
+
*/
|
|
31
|
+
/** Risk class for a tool, consumed by the 3d gate + MD-C policy. */
|
|
32
|
+
export type ToolCapability = 'exec' | 'high-blast' | 'low-risk';
|
|
33
|
+
/** Fail-safe class for an unrecognized tool name (never bypass the unknown). */
|
|
34
|
+
export declare const UNKNOWN_DEFAULT: ToolCapability;
|
|
35
|
+
/**
|
|
36
|
+
* Shell / arbitrary-code execution — hard-blocked at `restricted` (MD-C).
|
|
37
|
+
* CONTENT owned by tempo-security (signed off 2026-06-04).
|
|
38
|
+
*
|
|
39
|
+
* CANONICAL exec denylist (single source of truth). The F1 refactor (3d) made
|
|
40
|
+
* `src/pi/extension.ts` import this set via `classify(name) === 'exec'` and
|
|
41
|
+
* REMOVED its former local `SHELL_TOOL_NAMES` — so the live restricted-mode gate
|
|
42
|
+
* now hard-blocks the full set including `powershell`/`pwsh`/`cmd`/`run` (the gap
|
|
43
|
+
* the old local list left open). Never re-declare a shell denylist elsewhere.
|
|
44
|
+
*/
|
|
45
|
+
export declare const EXEC_TOOLS: ReadonlySet<string>;
|
|
46
|
+
/**
|
|
47
|
+
* Destructive / exfiltration-capable — gated when an operator is armed.
|
|
48
|
+
* CONTENT owned by tempo-security (signed off 2026-06-04).
|
|
49
|
+
*/
|
|
50
|
+
export declare const HIGH_BLAST_TOOLS: ReadonlySet<string>;
|
|
51
|
+
/**
|
|
52
|
+
* Read-only / coordination — always bypasses the gate.
|
|
53
|
+
* CONTENT owned by tempo-security (signed off 2026-06-04).
|
|
54
|
+
*/
|
|
55
|
+
export declare const LOW_RISK_TOOLS: ReadonlySet<string>;
|
|
56
|
+
/**
|
|
57
|
+
* Classify a tool name into its risk class. Pure + case-insensitive; unknown
|
|
58
|
+
* names fall through to {@link UNKNOWN_DEFAULT} (fail-safe — never bypass).
|
|
59
|
+
*/
|
|
60
|
+
export declare function classify(toolName: string): ToolCapability;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tool capability classifier (3d / MD-C) — a PURE function mapping a tool name
|
|
4
|
+
* to its risk class. The 3d operator gate and the MD-C headless tool-access
|
|
5
|
+
* posture consume it; this module owns the MECHANISM, tempo-security owns the
|
|
6
|
+
* CONTENT (the name-sets below) and signs off on it.
|
|
7
|
+
*
|
|
8
|
+
* - `'exec'` — shell / arbitrary-code execution. HARD-BLOCKED when the
|
|
9
|
+
* headless tool-access policy is `restricted` (MD-C). The
|
|
10
|
+
* highest-danger class.
|
|
11
|
+
* - `'high-blast'` — destructive or exfiltration-capable actions (file writes,
|
|
12
|
+
* network fetch, ensemble mutation). Allowed unsupervised,
|
|
13
|
+
* but routed to the OPERATOR GATE when an operator is armed.
|
|
14
|
+
* - `'low-risk'` — read-only / coordination. Always bypasses the gate.
|
|
15
|
+
*
|
|
16
|
+
* UNKNOWN tool names default to {@link UNKNOWN_DEFAULT} — a FAIL-SAFE choice
|
|
17
|
+
* (`'high-blast'`: never silently bypass a tool we don't recognize; gate it when
|
|
18
|
+
* an operator is armed). Whether an unknown tool should instead hard-block
|
|
19
|
+
* (`'exec'`) is a tempo-security posture call — see the cue to security.
|
|
20
|
+
*
|
|
21
|
+
* Matching is CASE-INSENSITIVE on the trimmed tool name. Pi's `tool_call`
|
|
22
|
+
* `toolName` is a bare string (built-ins like `bash`/`read`/`edit`/`write`/`grep`
|
|
23
|
+
* plus the natively-registered agent-tempo MCP tools), so we normalize before
|
|
24
|
+
* lookup.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ NAME-SET OWNERSHIP: the three Sets are tempo-security's posture content.
|
|
27
|
+
* They are a STARTER set (conductor's examples + Pi built-ins + the agent-tempo
|
|
28
|
+
* coordination/mutation tools) pending security's authoritative sign-off; amend
|
|
29
|
+
* the Set membership without touching the mechanism. The classifier behavior
|
|
30
|
+
* (and its tests) are keyed on representative names that won't move.
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.LOW_RISK_TOOLS = exports.HIGH_BLAST_TOOLS = exports.EXEC_TOOLS = exports.UNKNOWN_DEFAULT = void 0;
|
|
34
|
+
exports.classify = classify;
|
|
35
|
+
/** Fail-safe class for an unrecognized tool name (never bypass the unknown). */
|
|
36
|
+
exports.UNKNOWN_DEFAULT = 'high-blast';
|
|
37
|
+
/**
|
|
38
|
+
* Shell / arbitrary-code execution — hard-blocked at `restricted` (MD-C).
|
|
39
|
+
* CONTENT owned by tempo-security (signed off 2026-06-04).
|
|
40
|
+
*
|
|
41
|
+
* CANONICAL exec denylist (single source of truth). The F1 refactor (3d) made
|
|
42
|
+
* `src/pi/extension.ts` import this set via `classify(name) === 'exec'` and
|
|
43
|
+
* REMOVED its former local `SHELL_TOOL_NAMES` — so the live restricted-mode gate
|
|
44
|
+
* now hard-blocks the full set including `powershell`/`pwsh`/`cmd`/`run` (the gap
|
|
45
|
+
* the old local list left open). Never re-declare a shell denylist elsewhere.
|
|
46
|
+
*/
|
|
47
|
+
exports.EXEC_TOOLS = new Set([
|
|
48
|
+
'bash',
|
|
49
|
+
'shell',
|
|
50
|
+
'exec',
|
|
51
|
+
'sh',
|
|
52
|
+
'powershell',
|
|
53
|
+
'pwsh',
|
|
54
|
+
'cmd',
|
|
55
|
+
'run',
|
|
56
|
+
'process',
|
|
57
|
+
'command',
|
|
58
|
+
'run_command',
|
|
59
|
+
]);
|
|
60
|
+
/**
|
|
61
|
+
* Destructive / exfiltration-capable — gated when an operator is armed.
|
|
62
|
+
* CONTENT owned by tempo-security (signed off 2026-06-04).
|
|
63
|
+
*/
|
|
64
|
+
exports.HIGH_BLAST_TOOLS = new Set([
|
|
65
|
+
// Pi built-in mutating tools.
|
|
66
|
+
'write',
|
|
67
|
+
'edit',
|
|
68
|
+
'multiedit',
|
|
69
|
+
// Network exfiltration surface (arbitrary HTTP incl. POST). NOTE: `websearch`
|
|
70
|
+
// is LOW_RISK (read-only results) — only the fetch/browse family gates.
|
|
71
|
+
'webfetch',
|
|
72
|
+
'web_fetch',
|
|
73
|
+
'fetch',
|
|
74
|
+
'http_request',
|
|
75
|
+
'browser',
|
|
76
|
+
// agent-tempo tools that mutate the ensemble / spawn / tear down / control peers.
|
|
77
|
+
'recruit',
|
|
78
|
+
'destroy',
|
|
79
|
+
'restart',
|
|
80
|
+
'migrate',
|
|
81
|
+
'shutdown',
|
|
82
|
+
'release', // releases a held player — state change on a peer
|
|
83
|
+
'broadcast', // fans out to ALL players (amplification)
|
|
84
|
+
'schedule', // creates a durable autonomous trigger (unschedule is LOW_RISK)
|
|
85
|
+
'pause',
|
|
86
|
+
'play',
|
|
87
|
+
'restore', // re-spawns a detached player (process spawn)
|
|
88
|
+
// State / artifact destruction (irreversible).
|
|
89
|
+
'save_state',
|
|
90
|
+
'clear_state',
|
|
91
|
+
'coat_check_evict',
|
|
92
|
+
// Lineup spawn (full ensemble from YAML; save_lineup is LOW_RISK).
|
|
93
|
+
'load_lineup',
|
|
94
|
+
// Git state + disk mutation.
|
|
95
|
+
'worktree',
|
|
96
|
+
'stage',
|
|
97
|
+
'cancel_stage', // discards staged work (irreversible)
|
|
98
|
+
// Quality gates that block/unblock ensemble workflow progress.
|
|
99
|
+
'quality_gate',
|
|
100
|
+
'evaluate_gate',
|
|
101
|
+
// Drive/file mutation.
|
|
102
|
+
'copy_file',
|
|
103
|
+
]);
|
|
104
|
+
/**
|
|
105
|
+
* Read-only / coordination — always bypasses the gate.
|
|
106
|
+
* CONTENT owned by tempo-security (signed off 2026-06-04).
|
|
107
|
+
*/
|
|
108
|
+
exports.LOW_RISK_TOOLS = new Set([
|
|
109
|
+
// Pi built-in read tools.
|
|
110
|
+
'read',
|
|
111
|
+
'grep',
|
|
112
|
+
'glob',
|
|
113
|
+
'ls',
|
|
114
|
+
// Read-only web search (no arbitrary POST — distinct from the web_fetch family).
|
|
115
|
+
'websearch',
|
|
116
|
+
'web_search',
|
|
117
|
+
// agent-tempo read-only / status / messaging coordination tools.
|
|
118
|
+
'cue',
|
|
119
|
+
'report',
|
|
120
|
+
'recall',
|
|
121
|
+
'listen',
|
|
122
|
+
'ensemble',
|
|
123
|
+
'who_am_i',
|
|
124
|
+
'set_name',
|
|
125
|
+
'set_part',
|
|
126
|
+
'set_ensemble_description', // metadata only, no blast radius
|
|
127
|
+
'hosts',
|
|
128
|
+
'attachment_info',
|
|
129
|
+
'agent_types',
|
|
130
|
+
'schedules',
|
|
131
|
+
'stages',
|
|
132
|
+
'gates',
|
|
133
|
+
// Read-only state / coat-check (coat_check_put is bounded 32KB + TTL'd + visible).
|
|
134
|
+
'fetch_state',
|
|
135
|
+
'coat_check_put',
|
|
136
|
+
'coat_check_get',
|
|
137
|
+
'coat_check_list',
|
|
138
|
+
// Removes a future trigger — blast was at schedule-time, not here.
|
|
139
|
+
'unschedule',
|
|
140
|
+
// Writes the coordinator YAML (not arbitrary file write; blast is at load_lineup).
|
|
141
|
+
'save_lineup',
|
|
142
|
+
]);
|
|
143
|
+
/**
|
|
144
|
+
* Classify a tool name into its risk class. Pure + case-insensitive; unknown
|
|
145
|
+
* names fall through to {@link UNKNOWN_DEFAULT} (fail-safe — never bypass).
|
|
146
|
+
*/
|
|
147
|
+
function classify(toolName) {
|
|
148
|
+
const name = toolName.trim().toLowerCase();
|
|
149
|
+
if (exports.EXEC_TOOLS.has(name))
|
|
150
|
+
return 'exec';
|
|
151
|
+
if (exports.HIGH_BLAST_TOOLS.has(name))
|
|
152
|
+
return 'high-blast';
|
|
153
|
+
if (exports.LOW_RISK_TOOLS.has(name))
|
|
154
|
+
return 'low-risk';
|
|
155
|
+
return exports.UNKNOWN_DEFAULT;
|
|
156
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin CLIENT-SIDE Temporal wrapper for the Pi extension (D4 = (a)).
|
|
3
|
+
*
|
|
4
|
+
* The durable Temporal `Worker` stays in the daemon. This holds only a
|
|
5
|
+
* `WorkflowClient` that signals/queries/updates the session workflow — it never
|
|
6
|
+
* runs workflow code, so NOTHING here (or anywhere in src/pi/) is imported into
|
|
7
|
+
* the V8 workflow sandbox. The determinism boundary is preserved.
|
|
8
|
+
*
|
|
9
|
+
* Reuses the existing wire surface verbatim:
|
|
10
|
+
* - `claimAttachment` / `heartbeat` — lease lifecycle (drives `attached`)
|
|
11
|
+
* - `processingStart` / `processingEnd` — drives `processing` / `awaiting`
|
|
12
|
+
* - `requestDetach` + `adapterExited` — drives `draining` → `detached`
|
|
13
|
+
* - `submitOutbox` — report routing (outbox compliance)
|
|
14
|
+
* - `pendingMessages` + `markDelivered` — cue intake + ack
|
|
15
|
+
*/
|
|
16
|
+
import { Client, type WorkflowHandle } from '@temporalio/client';
|
|
17
|
+
import { type Config } from '../config';
|
|
18
|
+
import type { AttachmentToken, DetachReason, Message, OutboxEntryInput, PendingReset, SessionMetadata } from '../types';
|
|
19
|
+
import type { WorkflowAction } from './phase-driver';
|
|
20
|
+
/**
|
|
21
|
+
* MD-A liveness timings (Phase 2, maintainer-approved): 30s heartbeat / 90s
|
|
22
|
+
* lease, holding the **lease = 3×heartbeat** invariant — a single (or even
|
|
23
|
+
* second) missed beat never expires the lease, but a dead process is reaped
|
|
24
|
+
* within ~one lease window. Parity with the other SDK adapters' `heartbeatMs`.
|
|
25
|
+
* Overridable per-session via `PiWorkflowClientOptions.{leaseMs,heartbeatMs}`.
|
|
26
|
+
* Exported so a guard test can assert the invariant doesn't silently drift.
|
|
27
|
+
*/
|
|
28
|
+
export declare const PI_HEARTBEAT_MS = 30000;
|
|
29
|
+
export declare const PI_LEASE_MS = 90000;
|
|
30
|
+
/** 3c Tier-1 — coarse activity sample piggybacked onto each heartbeat. */
|
|
31
|
+
export type CoarseSample = {
|
|
32
|
+
currentTool: string | null;
|
|
33
|
+
contextTokens?: number;
|
|
34
|
+
contextPercent?: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Supplies the latest coarse sample at heartbeat time. eng's inner-loop
|
|
38
|
+
* publisher exposes exactly this via `getCoarseState()`; the extension wires it
|
|
39
|
+
* in after constructing the publisher. Absent (default) → heartbeats carry no
|
|
40
|
+
* coarse fields (backward-compatible with the 3a sender).
|
|
41
|
+
*/
|
|
42
|
+
export type CoarseProvider = () => CoarseSample | undefined;
|
|
43
|
+
export interface PiWorkflowClientOptions {
|
|
44
|
+
client: Client;
|
|
45
|
+
config: Config;
|
|
46
|
+
metadata: SessionMetadata;
|
|
47
|
+
leaseMs?: number;
|
|
48
|
+
heartbeatMs?: number;
|
|
49
|
+
/** 3c — optional coarse-activity provider (eng's `publisher.getCoarseState`). */
|
|
50
|
+
coarseProvider?: CoarseProvider;
|
|
51
|
+
/**
|
|
52
|
+
* Restart/migrate handoff token (`AGENT_TEMPO_ATTACHMENT_ID`). When present,
|
|
53
|
+
* `claim()` ADOPTS the pre-created attachment via the renewal branch instead of
|
|
54
|
+
* racing a fresh claim — the clean anti-flap path the restart tool will use
|
|
55
|
+
* (the read side of that spawn wiring is a tracked Phase 3 / restart carry-item;
|
|
56
|
+
* this is the forward-compatible plumbing). Absent on a fresh recruit / manual
|
|
57
|
+
* launch → fresh claim. Mirrors the existing adapters (`base.ts` startV2Lifecycle).
|
|
58
|
+
*/
|
|
59
|
+
expectedAttachmentId?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* One instance per attached Pi session — holds the session `WorkflowHandle`,
|
|
63
|
+
* lease token, and heartbeat timer for one fixed-workflowId player. Tool
|
|
64
|
+
* handlers reach this player's handle via the D11 lazy proxy (`get handle()`).
|
|
65
|
+
*/
|
|
66
|
+
export declare class PiWorkflowClient {
|
|
67
|
+
private readonly client;
|
|
68
|
+
private readonly config;
|
|
69
|
+
private readonly metadata;
|
|
70
|
+
private readonly leaseMs;
|
|
71
|
+
private readonly heartbeatMs;
|
|
72
|
+
private readonly expectedAttachmentId?;
|
|
73
|
+
private coarseProvider?;
|
|
74
|
+
private wfHandle;
|
|
75
|
+
private token;
|
|
76
|
+
private heartbeatTimer;
|
|
77
|
+
constructor(opts: PiWorkflowClientOptions);
|
|
78
|
+
/**
|
|
79
|
+
* 3c — wire the coarse-activity provider after construction (the inner-loop
|
|
80
|
+
* publisher is created alongside, then handed in). Each heartbeat samples it.
|
|
81
|
+
*/
|
|
82
|
+
setCoarseProvider(provider: CoarseProvider): void;
|
|
83
|
+
/** Build a client from config (connection-pooled; safe to share — see D12a). */
|
|
84
|
+
static connect(config?: Config): Promise<Client>;
|
|
85
|
+
get attachmentId(): string | null;
|
|
86
|
+
/**
|
|
87
|
+
* The live session `WorkflowHandle`, or `null` before {@link ensureSessionWorkflow}.
|
|
88
|
+
* Exposed so the D11 lazy-proxy (src/pi/lazy-proxy.ts) can resolve the player's
|
|
89
|
+
* OWN handle per tool call — tool handlers route through it (e.g. `submitOutbox`)
|
|
90
|
+
* and never `.signal()` a peer workflow directly.
|
|
91
|
+
*/
|
|
92
|
+
get handle(): WorkflowHandle | null;
|
|
93
|
+
private get workflowId();
|
|
94
|
+
/**
|
|
95
|
+
* Ensure the session workflow exists and grab a handle. For a human-launched
|
|
96
|
+
* `pi`, the workflow may not exist yet; `USE_EXISTING` makes this idempotent
|
|
97
|
+
* when `agent-tempo up`/recruit already started it.
|
|
98
|
+
*/
|
|
99
|
+
ensureSessionWorkflow(): Promise<WorkflowHandle>;
|
|
100
|
+
private requireHandle;
|
|
101
|
+
/**
|
|
102
|
+
* Claim (or, with a handoff token, ADOPT) the attachment: phase
|
|
103
|
+
* `booting → attached`. Fresh claim when `expectedAttachmentId` is absent;
|
|
104
|
+
* renewal/adoption branch when the restart tool handed one in. Stores the token.
|
|
105
|
+
*/
|
|
106
|
+
claim(): Promise<AttachmentToken>;
|
|
107
|
+
/**
|
|
108
|
+
* Liveness heartbeat — renews the lease. This is the ENTIRE reason an abrupt
|
|
109
|
+
* Pi death is detectable: stop heart-beating and `expiresAt` lapses (see
|
|
110
|
+
* src/pi/README.md "Abrupt-death finding" / MD-A).
|
|
111
|
+
*/
|
|
112
|
+
heartbeat(): Promise<void>;
|
|
113
|
+
/** Start the heartbeat loop. The timer is `unref`'d so it never holds the process open. */
|
|
114
|
+
startHeartbeat(): void;
|
|
115
|
+
stopHeartbeat(): void;
|
|
116
|
+
/** agent_start → phase `processing`. */
|
|
117
|
+
processingStart(messageId: string): Promise<void>;
|
|
118
|
+
/** agent_end → phase `awaiting`. */
|
|
119
|
+
processingEnd(messageId: string): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Graceful self-initiated detach: stopHeartbeat → adapterExited (→ detached).
|
|
122
|
+
*
|
|
123
|
+
* `adapterExited` ALONE collapses any live phase (attached/processing/awaiting/
|
|
124
|
+
* draining) straight to `detached` — see the workflow handler at
|
|
125
|
+
* `session.ts` adapterExitedSignal (returns early only on detached/gone). No
|
|
126
|
+
* `requestDetach` is needed for a self-exit; that signal is the EXTERNAL ask to
|
|
127
|
+
* drain a running adapter someone else controls. This matches the proven
|
|
128
|
+
* `BaseAttachment.stopV2Lifecycle(graceful)` path. Idempotent-safe to call on
|
|
129
|
+
* `session_shutdown` and from the headless exit sequence.
|
|
130
|
+
*/
|
|
131
|
+
detach(reason?: DetachReason): Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* Stash the currently-active Pi SessionManager session id onto durable
|
|
134
|
+
* workflow metadata (`metadata.sessionId`). This is the MUTABLE resume pointer
|
|
135
|
+
* — SEPARATE from the fixed workflowId (identity). A later restart of this
|
|
136
|
+
* player reads it to resume the same conversation via Pi `continueSession`
|
|
137
|
+
* (reuses the #334 sessionId resume field). No-op when the id is absent.
|
|
138
|
+
*/
|
|
139
|
+
updateSessionId(sessionId: string | undefined): Promise<void>;
|
|
140
|
+
/** Route an outbox entry through the workflow outbox (player's own handle). */
|
|
141
|
+
submitOutbox(entry: OutboxEntryInput): Promise<string>;
|
|
142
|
+
/** Pull undelivered messages (cues) queued on the workflow. */
|
|
143
|
+
fetchPending(): Promise<Message[]>;
|
|
144
|
+
/** Acknowledge delivered cue ids so the workflow stops re-serving them. */
|
|
145
|
+
ackDelivered(messageIds: string[]): Promise<void>;
|
|
146
|
+
/** Poll the workflow's single-slot pending reset (null = none). */
|
|
147
|
+
fetchPendingReset(): Promise<PendingReset | null>;
|
|
148
|
+
/**
|
|
149
|
+
* Ack a performed reset by id — the workflow clears the slot ONLY if the id
|
|
150
|
+
* still matches (race-safe: a newer reset that landed mid-wipe is preserved).
|
|
151
|
+
*/
|
|
152
|
+
ackReset(resetId: string): Promise<void>;
|
|
153
|
+
/**
|
|
154
|
+
* Dispatch a {@link WorkflowAction} produced by {@link PhaseDriver}. Centralizes
|
|
155
|
+
* the action→wire-call mapping so the extension wiring stays declarative.
|
|
156
|
+
*/
|
|
157
|
+
performAction(action: WorkflowAction): Promise<void>;
|
|
158
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PiWorkflowClient = exports.PI_LEASE_MS = exports.PI_HEARTBEAT_MS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Thin CLIENT-SIDE Temporal wrapper for the Pi extension (D4 = (a)).
|
|
6
|
+
*
|
|
7
|
+
* The durable Temporal `Worker` stays in the daemon. This holds only a
|
|
8
|
+
* `WorkflowClient` that signals/queries/updates the session workflow — it never
|
|
9
|
+
* runs workflow code, so NOTHING here (or anywhere in src/pi/) is imported into
|
|
10
|
+
* the V8 workflow sandbox. The determinism boundary is preserved.
|
|
11
|
+
*
|
|
12
|
+
* Reuses the existing wire surface verbatim:
|
|
13
|
+
* - `claimAttachment` / `heartbeat` — lease lifecycle (drives `attached`)
|
|
14
|
+
* - `processingStart` / `processingEnd` — drives `processing` / `awaiting`
|
|
15
|
+
* - `requestDetach` + `adapterExited` — drives `draining` → `detached`
|
|
16
|
+
* - `submitOutbox` — report routing (outbox compliance)
|
|
17
|
+
* - `pendingMessages` + `markDelivered` — cue intake + ack
|
|
18
|
+
*/
|
|
19
|
+
const client_1 = require("@temporalio/client");
|
|
20
|
+
const connection_1 = require("../connection");
|
|
21
|
+
const config_1 = require("../config");
|
|
22
|
+
const signals_1 = require("../workflows/signals");
|
|
23
|
+
/** Adapter identity advertised on claim. Pi interactive players are `interactive`-class. */
|
|
24
|
+
const PI_ADAPTER_ID = 'pi';
|
|
25
|
+
/**
|
|
26
|
+
* MD-A liveness timings (Phase 2, maintainer-approved): 30s heartbeat / 90s
|
|
27
|
+
* lease, holding the **lease = 3×heartbeat** invariant — a single (or even
|
|
28
|
+
* second) missed beat never expires the lease, but a dead process is reaped
|
|
29
|
+
* within ~one lease window. Parity with the other SDK adapters' `heartbeatMs`.
|
|
30
|
+
* Overridable per-session via `PiWorkflowClientOptions.{leaseMs,heartbeatMs}`.
|
|
31
|
+
* Exported so a guard test can assert the invariant doesn't silently drift.
|
|
32
|
+
*/
|
|
33
|
+
exports.PI_HEARTBEAT_MS = 30_000;
|
|
34
|
+
exports.PI_LEASE_MS = 90_000;
|
|
35
|
+
const log = (...args) => {
|
|
36
|
+
// eslint-disable-next-line no-console
|
|
37
|
+
console.error('[agent-tempo:pi]', ...args);
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* One instance per attached Pi session — holds the session `WorkflowHandle`,
|
|
41
|
+
* lease token, and heartbeat timer for one fixed-workflowId player. Tool
|
|
42
|
+
* handlers reach this player's handle via the D11 lazy proxy (`get handle()`).
|
|
43
|
+
*/
|
|
44
|
+
class PiWorkflowClient {
|
|
45
|
+
client;
|
|
46
|
+
config;
|
|
47
|
+
metadata;
|
|
48
|
+
leaseMs;
|
|
49
|
+
heartbeatMs;
|
|
50
|
+
expectedAttachmentId;
|
|
51
|
+
coarseProvider;
|
|
52
|
+
wfHandle = null;
|
|
53
|
+
token = null;
|
|
54
|
+
heartbeatTimer = null;
|
|
55
|
+
constructor(opts) {
|
|
56
|
+
this.client = opts.client;
|
|
57
|
+
this.config = opts.config;
|
|
58
|
+
this.metadata = opts.metadata;
|
|
59
|
+
this.leaseMs = opts.leaseMs ?? exports.PI_LEASE_MS;
|
|
60
|
+
this.heartbeatMs = opts.heartbeatMs ?? exports.PI_HEARTBEAT_MS;
|
|
61
|
+
this.expectedAttachmentId = opts.expectedAttachmentId;
|
|
62
|
+
this.coarseProvider = opts.coarseProvider;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 3c — wire the coarse-activity provider after construction (the inner-loop
|
|
66
|
+
* publisher is created alongside, then handed in). Each heartbeat samples it.
|
|
67
|
+
*/
|
|
68
|
+
setCoarseProvider(provider) {
|
|
69
|
+
this.coarseProvider = provider;
|
|
70
|
+
}
|
|
71
|
+
/** Build a client from config (connection-pooled; safe to share — see D12a). */
|
|
72
|
+
static async connect(config = (0, config_1.getConfig)()) {
|
|
73
|
+
const connection = await (0, connection_1.createTemporalConnection)(config);
|
|
74
|
+
return new client_1.Client({ connection, namespace: config.temporalNamespace });
|
|
75
|
+
}
|
|
76
|
+
get attachmentId() {
|
|
77
|
+
return this.token?.attachmentId ?? null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The live session `WorkflowHandle`, or `null` before {@link ensureSessionWorkflow}.
|
|
81
|
+
* Exposed so the D11 lazy-proxy (src/pi/lazy-proxy.ts) can resolve the player's
|
|
82
|
+
* OWN handle per tool call — tool handlers route through it (e.g. `submitOutbox`)
|
|
83
|
+
* and never `.signal()` a peer workflow directly.
|
|
84
|
+
*/
|
|
85
|
+
get handle() {
|
|
86
|
+
return this.wfHandle;
|
|
87
|
+
}
|
|
88
|
+
get workflowId() {
|
|
89
|
+
return (0, config_1.sessionWorkflowId)(this.metadata.ensemble, this.metadata.playerId);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Ensure the session workflow exists and grab a handle. For a human-launched
|
|
93
|
+
* `pi`, the workflow may not exist yet; `USE_EXISTING` makes this idempotent
|
|
94
|
+
* when `agent-tempo up`/recruit already started it.
|
|
95
|
+
*/
|
|
96
|
+
async ensureSessionWorkflow() {
|
|
97
|
+
if (this.wfHandle)
|
|
98
|
+
return this.wfHandle;
|
|
99
|
+
this.wfHandle = await this.client.workflow.start('agentSessionWorkflow', {
|
|
100
|
+
workflowId: this.workflowId,
|
|
101
|
+
taskQueue: this.config.taskQueue,
|
|
102
|
+
workflowIdConflictPolicy: 'USE_EXISTING',
|
|
103
|
+
args: [{ metadata: this.metadata, phase: 'booting' }],
|
|
104
|
+
});
|
|
105
|
+
return this.wfHandle;
|
|
106
|
+
}
|
|
107
|
+
requireHandle() {
|
|
108
|
+
if (!this.wfHandle) {
|
|
109
|
+
throw new Error('PiWorkflowClient: call ensureSessionWorkflow() before lifecycle ops');
|
|
110
|
+
}
|
|
111
|
+
return this.wfHandle;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Claim (or, with a handoff token, ADOPT) the attachment: phase
|
|
115
|
+
* `booting → attached`. Fresh claim when `expectedAttachmentId` is absent;
|
|
116
|
+
* renewal/adoption branch when the restart tool handed one in. Stores the token.
|
|
117
|
+
*/
|
|
118
|
+
async claim() {
|
|
119
|
+
const handle = this.requireHandle();
|
|
120
|
+
this.token = await handle.executeUpdate(signals_1.claimAttachmentUpdate, {
|
|
121
|
+
args: [
|
|
122
|
+
{
|
|
123
|
+
host: this.metadata.hostname,
|
|
124
|
+
adapterId: PI_ADAPTER_ID,
|
|
125
|
+
adapterClass: 'interactive',
|
|
126
|
+
leaseMs: this.leaseMs,
|
|
127
|
+
...(this.expectedAttachmentId ? { expectedAttachmentId: this.expectedAttachmentId } : {}),
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
log(`${this.expectedAttachmentId ? 'renewed' : 'claimed'} attachment ` +
|
|
132
|
+
`${this.token.attachmentId} (lease ${this.leaseMs}ms)`);
|
|
133
|
+
return this.token;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Liveness heartbeat — renews the lease. This is the ENTIRE reason an abrupt
|
|
137
|
+
* Pi death is detectable: stop heart-beating and `expiresAt` lapses (see
|
|
138
|
+
* src/pi/README.md "Abrupt-death finding" / MD-A).
|
|
139
|
+
*/
|
|
140
|
+
async heartbeat() {
|
|
141
|
+
if (!this.token)
|
|
142
|
+
return;
|
|
143
|
+
const handle = this.requireHandle();
|
|
144
|
+
// 3c Tier-1 — piggyback the latest coarse sample (currentTool + context).
|
|
145
|
+
// Sampled per-beat; a throwing/absent provider degrades to a plain heartbeat.
|
|
146
|
+
let coarse;
|
|
147
|
+
try {
|
|
148
|
+
coarse = this.coarseProvider?.();
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
coarse = undefined;
|
|
152
|
+
}
|
|
153
|
+
await handle.signal(signals_1.heartbeatSignal, {
|
|
154
|
+
attachmentId: this.token.attachmentId,
|
|
155
|
+
at: new Date().toISOString(),
|
|
156
|
+
...(coarse ? coarse : {}),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/** Start the heartbeat loop. The timer is `unref`'d so it never holds the process open. */
|
|
160
|
+
startHeartbeat() {
|
|
161
|
+
if (this.heartbeatTimer)
|
|
162
|
+
return;
|
|
163
|
+
this.heartbeatTimer = setInterval(() => {
|
|
164
|
+
this.heartbeat().catch((err) => log('heartbeat failed:', err));
|
|
165
|
+
}, this.heartbeatMs);
|
|
166
|
+
if (typeof this.heartbeatTimer.unref === 'function')
|
|
167
|
+
this.heartbeatTimer.unref();
|
|
168
|
+
}
|
|
169
|
+
stopHeartbeat() {
|
|
170
|
+
if (this.heartbeatTimer) {
|
|
171
|
+
clearInterval(this.heartbeatTimer);
|
|
172
|
+
this.heartbeatTimer = null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** agent_start → phase `processing`. */
|
|
176
|
+
async processingStart(messageId) {
|
|
177
|
+
const handle = this.requireHandle();
|
|
178
|
+
await handle.executeUpdate(signals_1.processingStartUpdate, {
|
|
179
|
+
args: [{ messageId, expectedAttachmentId: this.token?.attachmentId }],
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/** agent_end → phase `awaiting`. */
|
|
183
|
+
async processingEnd(messageId) {
|
|
184
|
+
const handle = this.requireHandle();
|
|
185
|
+
await handle.executeUpdate(signals_1.processingEndUpdate, {
|
|
186
|
+
args: [{ messageId, expectedAttachmentId: this.token?.attachmentId }],
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Graceful self-initiated detach: stopHeartbeat → adapterExited (→ detached).
|
|
191
|
+
*
|
|
192
|
+
* `adapterExited` ALONE collapses any live phase (attached/processing/awaiting/
|
|
193
|
+
* draining) straight to `detached` — see the workflow handler at
|
|
194
|
+
* `session.ts` adapterExitedSignal (returns early only on detached/gone). No
|
|
195
|
+
* `requestDetach` is needed for a self-exit; that signal is the EXTERNAL ask to
|
|
196
|
+
* drain a running adapter someone else controls. This matches the proven
|
|
197
|
+
* `BaseAttachment.stopV2Lifecycle(graceful)` path. Idempotent-safe to call on
|
|
198
|
+
* `session_shutdown` and from the headless exit sequence.
|
|
199
|
+
*/
|
|
200
|
+
async detach(reason = 'agent-exited') {
|
|
201
|
+
if (!this.wfHandle || !this.token)
|
|
202
|
+
return;
|
|
203
|
+
this.stopHeartbeat();
|
|
204
|
+
// Signal-DELIVERY (not a processing-ack) is sufficient by design, ruled won't-fix:
|
|
205
|
+
// the session workflow runs in the DAEMON worker, not this Pi process, so once
|
|
206
|
+
// signal() resolves the exit is durably in history and the worker transitions to
|
|
207
|
+
// `detached` regardless of this process disposing/exiting. stopHeartbeat() above is
|
|
208
|
+
// the independent backstop — a missed transition still reaps via lease expiry. No
|
|
209
|
+
// caller reads phase synchronously at detach()-return, so an executeUpdate ack would
|
|
210
|
+
// only add shutdown latency + drag determinism-sensitive workflow code into scope.
|
|
211
|
+
// If a future synchronous re-claim/migrate ever needs an OBSERVED `detached`, poll the
|
|
212
|
+
// existing attachmentInfoQuery client-side — never add an adapterExitedUpdate.
|
|
213
|
+
await this.wfHandle.signal(signals_1.adapterExitedSignal, {
|
|
214
|
+
attachmentId: this.token.attachmentId,
|
|
215
|
+
reason,
|
|
216
|
+
});
|
|
217
|
+
log(`detached (${reason})`);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Stash the currently-active Pi SessionManager session id onto durable
|
|
221
|
+
* workflow metadata (`metadata.sessionId`). This is the MUTABLE resume pointer
|
|
222
|
+
* — SEPARATE from the fixed workflowId (identity). A later restart of this
|
|
223
|
+
* player reads it to resume the same conversation via Pi `continueSession`
|
|
224
|
+
* (reuses the #334 sessionId resume field). No-op when the id is absent.
|
|
225
|
+
*/
|
|
226
|
+
async updateSessionId(sessionId) {
|
|
227
|
+
if (!sessionId)
|
|
228
|
+
return;
|
|
229
|
+
const handle = this.requireHandle();
|
|
230
|
+
await handle.signal(signals_1.updateMetadataSignal, { sessionId });
|
|
231
|
+
}
|
|
232
|
+
// ── Outbox ──
|
|
233
|
+
/** Route an outbox entry through the workflow outbox (player's own handle). */
|
|
234
|
+
async submitOutbox(entry) {
|
|
235
|
+
const handle = this.requireHandle();
|
|
236
|
+
return handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
|
|
237
|
+
}
|
|
238
|
+
// ── Cue intake ──
|
|
239
|
+
/** Pull undelivered messages (cues) queued on the workflow. */
|
|
240
|
+
async fetchPending() {
|
|
241
|
+
const handle = this.requireHandle();
|
|
242
|
+
return handle.query(signals_1.pendingMessagesQuery);
|
|
243
|
+
}
|
|
244
|
+
/** Acknowledge delivered cue ids so the workflow stops re-serving them. */
|
|
245
|
+
async ackDelivered(messageIds) {
|
|
246
|
+
if (messageIds.length === 0)
|
|
247
|
+
return;
|
|
248
|
+
const handle = this.requireHandle();
|
|
249
|
+
await handle.signal(signals_1.markDeliveredSignal, messageIds);
|
|
250
|
+
}
|
|
251
|
+
// ── Reset intake (3d D14) ──
|
|
252
|
+
/** Poll the workflow's single-slot pending reset (null = none). */
|
|
253
|
+
async fetchPendingReset() {
|
|
254
|
+
const handle = this.requireHandle();
|
|
255
|
+
return handle.query(signals_1.pendingResetQuery);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Ack a performed reset by id — the workflow clears the slot ONLY if the id
|
|
259
|
+
* still matches (race-safe: a newer reset that landed mid-wipe is preserved).
|
|
260
|
+
*/
|
|
261
|
+
async ackReset(resetId) {
|
|
262
|
+
const handle = this.requireHandle();
|
|
263
|
+
await handle.signal(signals_1.ackResetSignal, resetId);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Dispatch a {@link WorkflowAction} produced by {@link PhaseDriver}. Centralizes
|
|
267
|
+
* the action→wire-call mapping so the extension wiring stays declarative.
|
|
268
|
+
*/
|
|
269
|
+
async performAction(action) {
|
|
270
|
+
switch (action.kind) {
|
|
271
|
+
case 'claim':
|
|
272
|
+
await this.claim();
|
|
273
|
+
this.startHeartbeat();
|
|
274
|
+
return;
|
|
275
|
+
case 'processingStart':
|
|
276
|
+
await this.processingStart(action.messageId);
|
|
277
|
+
return;
|
|
278
|
+
case 'processingEnd':
|
|
279
|
+
await this.processingEnd(action.messageId);
|
|
280
|
+
return;
|
|
281
|
+
case 'detach':
|
|
282
|
+
await this.detach();
|
|
283
|
+
return;
|
|
284
|
+
case 'none':
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
exports.PiWorkflowClient = PiWorkflowClient;
|