@opengsd/gsd-pi 1.4.0-dev.5fc22c6f → 1.4.0-dev.c18009cd
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/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +58 -4
- package/dist/resources/extensions/gsd/artifact-verification.js +17 -5
- package/dist/resources/extensions/gsd/auto/closeout.js +45 -42
- package/dist/resources/extensions/gsd/auto/finalize.js +9 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +1 -1
- package/dist/resources/extensions/gsd/auto/run-unit.js +5 -2
- package/dist/resources/extensions/gsd/auto/schedule-wakeup.js +3 -0
- package/dist/resources/extensions/gsd/auto.js +10 -3
- package/dist/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.js +58 -9
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +174 -16
- package/dist/resources/extensions/gsd/gsd-db.js +52 -0
- package/dist/resources/extensions/gsd/migrate-external.js +13 -4
- package/dist/resources/extensions/gsd/milestone-merge-transaction.js +3 -3
- package/dist/resources/extensions/gsd/repo-identity.js +16 -0
- package/dist/resources/extensions/gsd/root-write-leak-guard.js +4 -2
- package/dist/resources/extensions/gsd/unit-registry.js +27 -2
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +67 -5
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/integrations/hermes/CONTEXT.md +47 -0
- package/integrations/hermes/README.md +21 -0
- package/integrations/hermes/docs/issue-1162-grilling.md +197 -0
- package/integrations/hermes/docs/setup.md +4 -0
- package/integrations/hermes/open_gsd_hermes/commands.py +141 -0
- package/integrations/hermes/open_gsd_hermes/config.py +2 -0
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +594 -31
- package/integrations/hermes/open_gsd_hermes/notifications.py +10 -1
- package/integrations/hermes/plugin.yaml +4 -0
- package/integrations/hermes/tests/test_config.py +6 -0
- package/integrations/hermes/tests/test_gsd_client_cache.py +3 -0
- package/integrations/hermes/tests/test_gsd_client_execute.py +35 -0
- package/integrations/hermes/tests/test_gsd_client_process_cwd.py +22 -0
- package/integrations/hermes/tests/test_gsd_client_timeout.py +98 -0
- package/integrations/hermes/tests/test_milestone_client.py +447 -0
- package/integrations/hermes/tests/test_new_milestone_command.py +313 -0
- package/integrations/hermes/tests/test_supervisor_fsm.py +40 -0
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/dist/session-manager.js +1 -1
- package/packages/daemon/dist/session-manager.js.map +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.js +4 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.js.map +1 -1
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/server.d.ts +18 -5
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +46 -5
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/session-manager.js +1 -1
- package/packages/mcp-server/dist/session-manager.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +3 -0
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +64 -3
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +212 -39
- package/src/resources/extensions/gsd/artifact-verification.ts +16 -4
- package/src/resources/extensions/gsd/auto/closeout.ts +54 -56
- package/src/resources/extensions/gsd/auto/finalize.ts +8 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/orchestrator.ts +1 -1
- package/src/resources/extensions/gsd/auto/run-unit.ts +5 -2
- package/src/resources/extensions/gsd/auto/schedule-wakeup.ts +8 -0
- package/src/resources/extensions/gsd/auto.ts +9 -3
- package/src/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.ts +80 -12
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +230 -21
- package/src/resources/extensions/gsd/gsd-db.ts +72 -0
- package/src/resources/extensions/gsd/migrate-external.ts +14 -4
- package/src/resources/extensions/gsd/milestone-merge-transaction.ts +4 -4
- package/src/resources/extensions/gsd/repo-identity.ts +17 -0
- package/src/resources/extensions/gsd/root-write-leak-guard.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +131 -5
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +335 -0
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +152 -22
- package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +50 -6
- package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +31 -9
- package/src/resources/extensions/gsd/tests/schedule-wakeup-tool.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/unit-registry.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +2 -2
- package/src/resources/extensions/gsd/unit-registry.ts +34 -2
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +135 -11
- /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Tests for /gsd new-milestone command routing and the cross-command guards.
|
|
2
|
+
|
|
3
|
+
Covers the issue #1162 design:
|
|
4
|
+
- _cmd_new_milestone: input parsing (bare text, --file, empty, --auto rejected),
|
|
5
|
+
co-run guard (reject if auto RUNNING/BLOCKED), spawn + ack, help text.
|
|
6
|
+
- _cmd_auto: symmetric co-run guard (reject if milestone run active).
|
|
7
|
+
- _cmd_cancel: routes to cancel_milestone() when a milestone subprocess is active.
|
|
8
|
+
- _cmd_reply: routes planning-blocker replies to client stdin when milestone
|
|
9
|
+
subprocess is active with a pending blocker.
|
|
10
|
+
|
|
11
|
+
Async handlers are driven with asyncio.run, matching the convention in
|
|
12
|
+
test_register.py.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from unittest.mock import MagicMock
|
|
20
|
+
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
from open_gsd_hermes.commands import GsdCommandRouter
|
|
24
|
+
from open_gsd_hermes.config import GsdConfig
|
|
25
|
+
from open_gsd_hermes.supervisor import SupervisorContext, SupervisorState
|
|
26
|
+
from open_gsd_hermes.types import BindingContext
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _make_router(
|
|
30
|
+
*,
|
|
31
|
+
project_dir: str | None = "/proj",
|
|
32
|
+
supervisor_state: SupervisorState = SupervisorState.IDLE,
|
|
33
|
+
milestone_active: bool = False,
|
|
34
|
+
milestone_blocker_id: str | None = None,
|
|
35
|
+
) -> tuple[GsdCommandRouter, MagicMock]:
|
|
36
|
+
"""Build a router with a MagicMock client + bind store for isolated testing."""
|
|
37
|
+
config = GsdConfig(default_project=project_dir)
|
|
38
|
+
client = MagicMock()
|
|
39
|
+
client.milestone_active.return_value = milestone_active
|
|
40
|
+
client.milestone_session_id.return_value = "S-1" if milestone_active else None
|
|
41
|
+
client.milestone_pending_blocker_id.return_value = milestone_blocker_id
|
|
42
|
+
bind_store = MagicMock()
|
|
43
|
+
supervisor = MagicMock()
|
|
44
|
+
notifications = MagicMock()
|
|
45
|
+
|
|
46
|
+
sup_ctx_holder: dict[str, SupervisorContext] = {
|
|
47
|
+
"ctx": SupervisorContext(
|
|
48
|
+
session_id=None,
|
|
49
|
+
project_dir=project_dir,
|
|
50
|
+
state=supervisor_state,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def get_session_key() -> str:
|
|
55
|
+
return "agent:main:cli:direct:local"
|
|
56
|
+
|
|
57
|
+
def get_binding_ctx() -> BindingContext:
|
|
58
|
+
return BindingContext(cwd=project_dir)
|
|
59
|
+
|
|
60
|
+
def get_supervisor_ctx() -> SupervisorContext:
|
|
61
|
+
return sup_ctx_holder["ctx"]
|
|
62
|
+
|
|
63
|
+
def set_supervisor_ctx(ctx: SupervisorContext) -> None:
|
|
64
|
+
sup_ctx_holder["ctx"] = ctx
|
|
65
|
+
|
|
66
|
+
def get_platform_channel() -> tuple[str | None, str | None]:
|
|
67
|
+
return "cli", "local"
|
|
68
|
+
|
|
69
|
+
router = GsdCommandRouter(
|
|
70
|
+
config,
|
|
71
|
+
client,
|
|
72
|
+
bind_store,
|
|
73
|
+
supervisor,
|
|
74
|
+
get_session_key,
|
|
75
|
+
get_binding_ctx,
|
|
76
|
+
get_supervisor_ctx,
|
|
77
|
+
set_supervisor_ctx,
|
|
78
|
+
get_platform_channel,
|
|
79
|
+
notifications,
|
|
80
|
+
)
|
|
81
|
+
return router, client
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.fixture
|
|
85
|
+
def project(tmp_path: Path) -> Path:
|
|
86
|
+
"""A directory that looks like a GSD project (.gsd/ present)."""
|
|
87
|
+
(tmp_path / ".gsd").mkdir()
|
|
88
|
+
return tmp_path
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def run(router: GsdCommandRouter, args: str) -> str:
|
|
92
|
+
return asyncio.run(router.handle(args))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# _cmd_new_milestone — input parsing & usage
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_new_milestone_bare_text_passes_context_text(project: Path) -> None:
|
|
101
|
+
router, client = _make_router(project_dir=str(project))
|
|
102
|
+
result = run(router, "new-milestone Build a REST API with auth")
|
|
103
|
+
client.create_milestone.assert_called_once()
|
|
104
|
+
kwargs = client.create_milestone.call_args.kwargs
|
|
105
|
+
assert kwargs.get("context_text") == "Build a REST API with auth"
|
|
106
|
+
assert kwargs.get("context_file") is None
|
|
107
|
+
assert "started" in result.lower() or "S-1" in result
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_new_milestone_terminal_callback_clears_local_session_id(
|
|
111
|
+
project: Path,
|
|
112
|
+
) -> None:
|
|
113
|
+
router, client = _make_router(project_dir=str(project))
|
|
114
|
+
client.create_milestone.return_value = "milestone-4242"
|
|
115
|
+
|
|
116
|
+
run(router, "new-milestone Build a REST API with auth")
|
|
117
|
+
ctx = router._get_supervisor_ctx() # type: ignore[attr-defined]
|
|
118
|
+
ctx.pending_blocker_id = "blk-1"
|
|
119
|
+
assert ctx.session_id == "milestone-4242"
|
|
120
|
+
|
|
121
|
+
on_terminal = client.create_milestone.call_args.kwargs["on_terminal"]
|
|
122
|
+
on_terminal("complete")
|
|
123
|
+
|
|
124
|
+
ctx = router._get_supervisor_ctx() # type: ignore[attr-defined]
|
|
125
|
+
assert ctx.state == SupervisorState.COMPLETE
|
|
126
|
+
assert ctx.session_id is None
|
|
127
|
+
assert ctx.pending_blocker_id is None
|
|
128
|
+
assert ctx.notified_terminal is True
|
|
129
|
+
|
|
130
|
+
result = run(router, "reply stale response")
|
|
131
|
+
client.resolve_blocker.assert_not_called()
|
|
132
|
+
assert "no active" in result.lower()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_new_milestone_file_flag_passes_context_file(project: Path) -> None:
|
|
136
|
+
router, client = _make_router(project_dir=str(project))
|
|
137
|
+
spec = project / "spec.md"
|
|
138
|
+
spec.write_text("milestone spec", encoding="utf-8")
|
|
139
|
+
run(router, f"new-milestone --file {spec}")
|
|
140
|
+
kwargs = client.create_milestone.call_args.kwargs
|
|
141
|
+
assert kwargs.get("context_file") == str(spec)
|
|
142
|
+
assert kwargs.get("context_text") is None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_new_milestone_file_flag_resolves_relative_path_from_project(
|
|
146
|
+
project: Path,
|
|
147
|
+
) -> None:
|
|
148
|
+
router, client = _make_router(project_dir=str(project))
|
|
149
|
+
spec = project / "spec.md"
|
|
150
|
+
spec.write_text("milestone spec", encoding="utf-8")
|
|
151
|
+
run(router, "new-milestone --file spec.md")
|
|
152
|
+
kwargs = client.create_milestone.call_args.kwargs
|
|
153
|
+
assert kwargs.get("context_file") == str(spec)
|
|
154
|
+
assert kwargs.get("context_text") is None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_new_milestone_file_flag_rejects_missing_file(project: Path) -> None:
|
|
158
|
+
router, client = _make_router(project_dir=str(project))
|
|
159
|
+
result = run(router, "new-milestone --file missing.md")
|
|
160
|
+
assert "not found" in result.lower()
|
|
161
|
+
client.create_milestone.assert_not_called()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_new_milestone_empty_returns_usage(project: Path) -> None:
|
|
165
|
+
router, client = _make_router(project_dir=str(project))
|
|
166
|
+
result = run(router, "new-milestone")
|
|
167
|
+
assert "usage" in result.lower()
|
|
168
|
+
client.create_milestone.assert_not_called()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_new_milestone_rejects_auto_flag(project: Path) -> None:
|
|
172
|
+
"""--auto chaining is out of scope; execution stays with /gsd auto."""
|
|
173
|
+
router, client = _make_router(project_dir=str(project))
|
|
174
|
+
result = run(router, "new-milestone --auto some spec")
|
|
175
|
+
assert "auto" in result.lower()
|
|
176
|
+
client.create_milestone.assert_not_called()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Co-run guards (Q2 + Q9 symmetry)
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_new_milestone_rejects_when_auto_running(project: Path) -> None:
|
|
185
|
+
"""Co-run guard: refuse if an auto session is RUNNING or BLOCKED."""
|
|
186
|
+
router, client = _make_router(
|
|
187
|
+
project_dir=str(project),
|
|
188
|
+
supervisor_state=SupervisorState.RUNNING,
|
|
189
|
+
)
|
|
190
|
+
result = run(router, "new-milestone some spec")
|
|
191
|
+
assert "cancel" in result.lower() or "running" in result.lower()
|
|
192
|
+
client.create_milestone.assert_not_called()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_new_milestone_rejects_when_auto_blocked(project: Path) -> None:
|
|
196
|
+
router, client = _make_router(
|
|
197
|
+
project_dir=str(project),
|
|
198
|
+
supervisor_state=SupervisorState.BLOCKED,
|
|
199
|
+
)
|
|
200
|
+
result = run(router, "new-milestone some spec")
|
|
201
|
+
assert "cancel" in result.lower() or "running" in result.lower()
|
|
202
|
+
client.create_milestone.assert_not_called()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_new_milestone_prioritizes_milestone_guard_over_auto_state(
|
|
206
|
+
project: Path,
|
|
207
|
+
) -> None:
|
|
208
|
+
"""A duplicate milestone command should report the milestone guard."""
|
|
209
|
+
router, client = _make_router(
|
|
210
|
+
project_dir=str(project),
|
|
211
|
+
supervisor_state=SupervisorState.RUNNING,
|
|
212
|
+
milestone_active=True,
|
|
213
|
+
)
|
|
214
|
+
result = run(router, "new-milestone some spec")
|
|
215
|
+
assert "milestone creation is in progress" in result.lower()
|
|
216
|
+
assert "auto is running" not in result.lower()
|
|
217
|
+
client.create_milestone.assert_not_called()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_new_milestone_allows_when_auto_complete(project: Path) -> None:
|
|
221
|
+
"""A terminal auto state does not block milestone creation."""
|
|
222
|
+
router, client = _make_router(
|
|
223
|
+
project_dir=str(project),
|
|
224
|
+
supervisor_state=SupervisorState.COMPLETE,
|
|
225
|
+
)
|
|
226
|
+
run(router, "new-milestone some spec")
|
|
227
|
+
client.create_milestone.assert_called_once()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_auto_rejects_when_milestone_active(project: Path) -> None:
|
|
231
|
+
"""Symmetric guard: /gsd auto refuses if a milestone run is in progress."""
|
|
232
|
+
router, client = _make_router(
|
|
233
|
+
project_dir=str(project),
|
|
234
|
+
milestone_active=True,
|
|
235
|
+
)
|
|
236
|
+
result = run(router, "auto")
|
|
237
|
+
assert "cancel" in result.lower() or "milestone" in result.lower()
|
|
238
|
+
client.execute.assert_not_called()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
# _cmd_cancel — milestone branch
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_cancel_routes_to_cancel_milestone_when_milestone_active(
|
|
247
|
+
project: Path,
|
|
248
|
+
) -> None:
|
|
249
|
+
router, client = _make_router(
|
|
250
|
+
project_dir=str(project),
|
|
251
|
+
milestone_active=True,
|
|
252
|
+
)
|
|
253
|
+
run(router, "cancel")
|
|
254
|
+
client.cancel_milestone.assert_called_once()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_cancel_routes_to_mcp_cancel_when_only_auto_active(project: Path) -> None:
|
|
258
|
+
"""When no milestone proc is active, cancel falls back to the MCP path."""
|
|
259
|
+
router, client = _make_router(
|
|
260
|
+
project_dir=str(project),
|
|
261
|
+
supervisor_state=SupervisorState.RUNNING,
|
|
262
|
+
)
|
|
263
|
+
# Give the auto session an id so the MCP cancel-by-session path is taken
|
|
264
|
+
run(router, "cancel")
|
|
265
|
+
client.cancel_milestone.assert_not_called()
|
|
266
|
+
assert client.cancel.called or client.cancel_by_project.called
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
# _cmd_reply — milestone-blocker branch
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_reply_routes_to_milestone_blocker_when_milestone_pending(
|
|
275
|
+
project: Path,
|
|
276
|
+
) -> None:
|
|
277
|
+
router, client = _make_router(
|
|
278
|
+
project_dir=str(project),
|
|
279
|
+
milestone_active=True,
|
|
280
|
+
milestone_blocker_id="blk-9",
|
|
281
|
+
)
|
|
282
|
+
result = run(router, "reply use option B")
|
|
283
|
+
client.respond_to_milestone_blocker.assert_called_once_with("use option B")
|
|
284
|
+
assert "sent" in result.lower()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_reply_does_not_fall_through_to_mcp_when_milestone_active_no_blocker(
|
|
288
|
+
project: Path,
|
|
289
|
+
) -> None:
|
|
290
|
+
"""Active milestone without a pending blocker must not hit MCP resolve_blocker."""
|
|
291
|
+
router, client = _make_router(
|
|
292
|
+
project_dir=str(project),
|
|
293
|
+
milestone_active=True,
|
|
294
|
+
milestone_blocker_id=None,
|
|
295
|
+
)
|
|
296
|
+
router._get_supervisor_ctx().session_id = "milestone-1" # type: ignore[attr-defined]
|
|
297
|
+
result = run(router, "reply some answer")
|
|
298
|
+
client.respond_to_milestone_blocker.assert_not_called()
|
|
299
|
+
client.resolve_blocker.assert_not_called()
|
|
300
|
+
assert "no pending blocker" in result.lower()
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def test_reply_falls_back_to_mcp_when_no_milestone(project: Path) -> None:
|
|
304
|
+
"""No milestone proc → existing MCP resolve_blocker path (needs a session)."""
|
|
305
|
+
router, client = _make_router(
|
|
306
|
+
project_dir=str(project),
|
|
307
|
+
supervisor_state=SupervisorState.BLOCKED,
|
|
308
|
+
)
|
|
309
|
+
# set a session id so the existing reply path doesn't short-circuit
|
|
310
|
+
router._get_supervisor_ctx().session_id = "auto-1" # type: ignore[attr-defined]
|
|
311
|
+
run(router, "reply some answer")
|
|
312
|
+
client.respond_to_milestone_blocker.assert_not_called()
|
|
313
|
+
client.resolve_blocker.assert_called_once()
|
|
@@ -138,6 +138,7 @@ def test_auto_resets_session_specific_supervisor_context(tmp_path) -> None:
|
|
|
138
138
|
)
|
|
139
139
|
supervisor = MockSupervisor()
|
|
140
140
|
client = MagicMock()
|
|
141
|
+
client.milestone_active.return_value = False
|
|
141
142
|
client.execute.return_value = {"sessionId": "new-session"}
|
|
142
143
|
|
|
143
144
|
router = GsdCommandRouter(
|
|
@@ -179,6 +180,7 @@ def test_status_returns_friendly_message_when_mcp_progress_fails(tmp_path) -> No
|
|
|
179
180
|
def test_auto_returns_friendly_message_when_mcp_execute_fails(tmp_path) -> None:
|
|
180
181
|
supervisor = MockSupervisor()
|
|
181
182
|
client = MagicMock()
|
|
183
|
+
client.milestone_active.return_value = False
|
|
182
184
|
client.execute.side_effect = McpProtocolError("sidecar unavailable")
|
|
183
185
|
router, _project_dir, _supervisor, _ctx, _stored = build_router(
|
|
184
186
|
tmp_path, client, supervisor=supervisor
|
|
@@ -193,6 +195,7 @@ def test_auto_returns_friendly_message_when_mcp_execute_fails(tmp_path) -> None:
|
|
|
193
195
|
def test_auto_rejects_missing_session_id_in_mcp_response(tmp_path) -> None:
|
|
194
196
|
supervisor = MockSupervisor()
|
|
195
197
|
client = MagicMock()
|
|
198
|
+
client.milestone_active.return_value = False
|
|
196
199
|
client.execute.return_value = {}
|
|
197
200
|
router, _project_dir, _supervisor, ctx, _stored = build_router(
|
|
198
201
|
tmp_path, client, supervisor=supervisor
|
|
@@ -209,6 +212,7 @@ def test_auto_rejects_missing_session_id_in_mcp_response(tmp_path) -> None:
|
|
|
209
212
|
def test_cancel_stops_supervisor_when_mcp_cancel_fails(tmp_path) -> None:
|
|
210
213
|
supervisor = MockSupervisor()
|
|
211
214
|
client = MagicMock()
|
|
215
|
+
client.milestone_active.return_value = False
|
|
212
216
|
client.cancel_by_project.side_effect = McpProtocolError("sidecar unavailable")
|
|
213
217
|
router, _project_dir, _supervisor, ctx, stored = build_router(
|
|
214
218
|
tmp_path, client, supervisor=supervisor
|
|
@@ -226,6 +230,7 @@ def test_cancel_sends_terminal_notification_after_successful_mcp_cancel(tmp_path
|
|
|
226
230
|
supervisor = MockSupervisor()
|
|
227
231
|
notifications = MagicMock()
|
|
228
232
|
client = MagicMock()
|
|
233
|
+
client.milestone_active.return_value = False
|
|
229
234
|
ctx = SupervisorContext(
|
|
230
235
|
session_id="s1",
|
|
231
236
|
state=SupervisorState.RUNNING,
|
|
@@ -261,6 +266,7 @@ def test_cancel_prefers_supervisor_project_for_active_session(tmp_path) -> None:
|
|
|
261
266
|
(running_project / ".gsd").mkdir()
|
|
262
267
|
supervisor = MockSupervisor()
|
|
263
268
|
client = MagicMock()
|
|
269
|
+
client.milestone_active.return_value = False
|
|
264
270
|
ctx = SupervisorContext(
|
|
265
271
|
session_id="s1",
|
|
266
272
|
project_dir=str(running_project),
|
|
@@ -291,6 +297,7 @@ def test_cancel_prefers_supervisor_project_for_active_session(tmp_path) -> None:
|
|
|
291
297
|
def test_cancel_stops_supervisor_when_binding_resolution_fails(tmp_path) -> None:
|
|
292
298
|
supervisor = MockSupervisor()
|
|
293
299
|
client = MagicMock()
|
|
300
|
+
client.milestone_active.return_value = False
|
|
294
301
|
ctx = SupervisorContext(
|
|
295
302
|
session_id="s1",
|
|
296
303
|
project_dir=str(tmp_path / "missing"),
|
|
@@ -323,6 +330,7 @@ def test_cancel_stops_supervisor_when_binding_resolution_fails(tmp_path) -> None
|
|
|
323
330
|
|
|
324
331
|
def test_reply_returns_friendly_message_when_mcp_resolve_fails(tmp_path) -> None:
|
|
325
332
|
client = MagicMock()
|
|
333
|
+
client.milestone_active.return_value = False
|
|
326
334
|
client.resolve_blocker.side_effect = McpProtocolError("sidecar unavailable")
|
|
327
335
|
ctx = SupervisorContext(session_id="s1", project_dir="/tmp/project")
|
|
328
336
|
router, _project_dir, _supervisor, _ctx, _stored = build_router(
|
|
@@ -370,6 +378,38 @@ def test_supervisor_detects_blocker() -> None:
|
|
|
370
378
|
assert any("blocker" in t.lower() or "Approve" in t for t in sent)
|
|
371
379
|
|
|
372
380
|
|
|
381
|
+
def test_notify_blocker_prefers_title_over_generic_message() -> None:
|
|
382
|
+
sent: list[str] = []
|
|
383
|
+
|
|
384
|
+
def dispatch(_name: str, args: dict) -> None:
|
|
385
|
+
sent.append(args.get("text", ""))
|
|
386
|
+
|
|
387
|
+
notifications = NotificationService(
|
|
388
|
+
MagicMock(),
|
|
389
|
+
GsdConfig(),
|
|
390
|
+
lambda: DeliveryTarget("slack", "channel", "C123"),
|
|
391
|
+
dispatch=dispatch,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
notifications.notify_blocker(
|
|
395
|
+
SessionStatus(
|
|
396
|
+
status="blocked",
|
|
397
|
+
pending_blocker={
|
|
398
|
+
"type": "extension_ui_request",
|
|
399
|
+
"method": "select",
|
|
400
|
+
"id": "sel-1",
|
|
401
|
+
"title": "Choose milestone scope",
|
|
402
|
+
"message": "Action required",
|
|
403
|
+
"options": ["A", "B"],
|
|
404
|
+
},
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
assert len(sent) == 1
|
|
409
|
+
assert "GSD blocker: Choose milestone scope" in sent[0]
|
|
410
|
+
assert "GSD blocker: Action required" not in sent[0]
|
|
411
|
+
|
|
412
|
+
|
|
373
413
|
def test_supervisor_terminal_tick_updates_progress_before_stopping() -> None:
|
|
374
414
|
ctx = SupervisorContext(
|
|
375
415
|
session_id="s1",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengsd/cloud-mcp-gateway",
|
|
3
|
-
"version": "1.4.0-dev.
|
|
3
|
+
"version": "1.4.0-dev.c18009cd",
|
|
4
4
|
"description": "Cloud MCP gateway for brokering remote MCP clients to local GSD runtimes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
32
|
-
"@opengsd/mcp-server": "^1.4.0-dev.
|
|
32
|
+
"@opengsd/mcp-server": "^1.4.0-dev.c18009cd",
|
|
33
33
|
"ws": "^8.20.0",
|
|
34
34
|
"zod": "^4.0.0"
|
|
35
35
|
},
|
|
@@ -97,7 +97,7 @@ export class SessionManager extends EventEmitter {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
const cliPath = options.cliPath ?? SessionManager.resolveCLIPath();
|
|
100
|
-
const args = [
|
|
100
|
+
const args = [];
|
|
101
101
|
if (options.model)
|
|
102
102
|
args.push('--model', options.model);
|
|
103
103
|
if (options.bare)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-manager.js","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SessionManager — manages RpcClient lifecycle for daemon-driven GSD execution.\n *\n * Extends EventEmitter to emit typed session lifecycle events.\n * One active session per projectDir. Tracks events in a ring buffer,\n * detects blockers, tracks terminal state, and accumulates cost using\n * the cumulative-max pattern (K004).\n *\n * Adapted from packages/mcp-server/src/session-manager.ts with:\n * - Logger integration for structured logging\n * - EventEmitter for session lifecycle events\n * - getAllSessions() for cross-project status (R035)\n * - projectName field on ManagedSession\n */\n\nimport { execSync } from 'node:child_process';\nimport { basename, resolve } from 'node:path';\nimport { EventEmitter } from 'node:events';\nimport { RpcClient } from '@opengsd/rpc-client';\nimport type { RpcCostUpdateEvent, RpcExtensionUIRequest, RpcInitResult, SdkAgentEvent } from '@opengsd/contracts';\nimport type {\n ManagedSession,\n StartSessionOptions,\n PendingBlocker,\n} from './types.js';\nimport { MAX_EVENTS, INIT_TIMEOUT_MS } from './types.js';\nimport type { Logger } from './logger.js';\n\n// ---------------------------------------------------------------------------\n// Inlined detection logic (from headless-events.ts — no internal package imports)\n// ---------------------------------------------------------------------------\n\nconst FIRE_AND_FORGET_METHODS = new Set([\n 'notify', 'setStatus', 'setWidget', 'setTitle', 'set_editor_text',\n]);\n\nconst TERMINAL_PREFIXES = [\n 'auto-mode stopped',\n 'step-mode stopped',\n 'auto-mode complete',\n 'no active milestone',\n 'auto-mode idle',\n];\n\nfunction isTerminalNotification(event: Record<string, unknown>): boolean {\n if (event.type !== 'extension_ui_request' || event.method !== 'notify') return false;\n const message = String(event.message ?? '').toLowerCase();\n return TERMINAL_PREFIXES.some((prefix) => message.startsWith(prefix));\n}\n\nfunction isBlockedNotification(event: Record<string, unknown>): boolean {\n if (event.type !== 'extension_ui_request' || event.method !== 'notify') return false;\n const message = String(event.message ?? '').toLowerCase();\n return message.includes('blocked:');\n}\n\nfunction isBlockingUIRequest(event: Record<string, unknown>): boolean {\n if (event.type !== 'extension_ui_request') return false;\n const method = String(event.method ?? '');\n return !FIRE_AND_FORGET_METHODS.has(method);\n}\n\n// ---------------------------------------------------------------------------\n// SessionManager\n// ---------------------------------------------------------------------------\n\n/** Terminal lifecycle states — work is finished, process may be reclaimed. */\nconst TERMINAL_STATUSES: ReadonlySet<string> = new Set(['completed', 'error', 'cancelled']);\n\n/**\n * How many terminal (completed/cancelled/errored) sessions to retain for\n * inspection before evicting the oldest. Bounds heap growth on a daemon that\n * services many distinct project dirs over its lifetime — without this, every\n * session ever run stays resident forever (events buffer + RpcClient ref).\n */\nconst MAX_TERMINAL_SESSIONS = 50;\n\nexport class SessionManager extends EventEmitter {\n /** Sessions keyed by resolved projectDir for duplicate-start prevention */\n private sessions = new Map<string, ManagedSession>();\n\n /** Resolved dirs of terminal sessions, oldest-first — for bounded retention. */\n private terminalOrder: string[] = [];\n\n constructor(private readonly logger: Logger) {\n super();\n }\n\n /**\n * Start a new GSD auto-mode session for the given project directory.\n *\n * Rejects if a session already exists for this projectDir.\n * Creates an RpcClient, starts the process, performs the v2 init handshake,\n * wires event tracking, and sends '/gsd auto' to begin execution.\n */\n async startSession(options: StartSessionOptions): Promise<string> {\n const { projectDir } = options;\n\n if (!projectDir || projectDir.trim() === '') {\n throw new Error('projectDir is required and cannot be empty');\n }\n\n const resolvedDir = resolve(projectDir);\n const projectName = basename(resolvedDir);\n\n const existing = this.sessions.get(resolvedDir);\n if (existing) {\n // A terminal session (completed/cancelled/error) is retained only for\n // inspection — it must not block a fresh start for the same dir. Evict it\n // and reclaim its process before starting anew.\n if (TERMINAL_STATUSES.has(existing.status)) {\n this.evictSession(resolvedDir);\n } else {\n throw new Error(\n `Session already active for ${resolvedDir} (sessionId: ${existing.sessionId}, status: ${existing.status})`\n );\n }\n }\n\n const cliPath = options.cliPath ?? SessionManager.resolveCLIPath();\n\n const args: string[] = ['--mode', 'rpc'];\n if (options.model) args.push('--model', options.model);\n if (options.bare) args.push('--bare');\n\n const client = new RpcClient({\n cliPath,\n cwd: resolvedDir,\n args,\n });\n\n // Build the session shell before async operations so we can track state\n const session: ManagedSession = {\n sessionId: '', // filled after init\n projectDir: resolvedDir,\n projectName,\n status: 'starting',\n client,\n events: [],\n pendingBlocker: null,\n cost: { totalCost: 0, tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } },\n startTime: Date.now(),\n };\n\n // Insert into map early (keyed by dir) so concurrent starts are rejected\n this.sessions.set(resolvedDir, session);\n\n try {\n // Start the process with timeout\n await Promise.race([\n client.start(),\n timeout(INIT_TIMEOUT_MS, `RpcClient.start() timed out after ${INIT_TIMEOUT_MS}ms`),\n ]);\n\n // Perform v2 init handshake\n const initResult: RpcInitResult = await Promise.race([\n client.init(),\n timeout(INIT_TIMEOUT_MS, `RpcClient.init() timed out after ${INIT_TIMEOUT_MS}ms`),\n ]) as RpcInitResult;\n\n session.sessionId = initResult.sessionId;\n session.status = 'running';\n\n // Wire event tracking\n session.unsubscribe = client.onEvent((event: SdkAgentEvent) => {\n this.handleEvent(session, event);\n });\n\n // Kick off auto-mode\n const command = options.command ?? '/gsd auto';\n await client.prompt(command);\n\n this.logger.info('session started', { sessionId: session.sessionId, projectDir: resolvedDir });\n this.emit('session:started', { sessionId: session.sessionId, projectDir: resolvedDir, projectName });\n\n return session.sessionId;\n } catch (err) {\n session.status = 'error';\n session.error = err instanceof Error ? err.message : String(err);\n\n // Attempt cleanup\n try { await client.stop(); } catch { /* swallow cleanup errors */ }\n\n this.logger.error('session error', { sessionId: session.sessionId, projectDir: resolvedDir, error: session.error });\n this.emit('session:error', { sessionId: session.sessionId, projectDir: resolvedDir, projectName, error: session.error });\n\n // Keep session in map so callers can inspect the error, but under the\n // bounded terminal-retention policy so it is eventually reclaimed.\n this.markTerminal(resolvedDir);\n throw new Error(`Failed to start session for ${resolvedDir}: ${session.error}`);\n }\n }\n\n /**\n * Look up a session by sessionId.\n * Linear scan is fine — we expect <10 concurrent sessions.\n */\n getSession(sessionId: string): ManagedSession | undefined {\n for (const session of this.sessions.values()) {\n if (session.sessionId === sessionId) return session;\n }\n return undefined;\n }\n\n /**\n * Look up a session by project directory (direct map lookup).\n */\n getSessionByDir(projectDir: string): ManagedSession | undefined {\n return this.sessions.get(resolve(projectDir));\n }\n\n /**\n * Return all tracked sessions (R035 — cross-project status).\n */\n getAllSessions(): ManagedSession[] {\n return Array.from(this.sessions.values());\n }\n\n /**\n * Resolve a pending blocker by sending a UI response.\n */\n async resolveBlocker(sessionId: string, response: string): Promise<void> {\n const session = this.getSession(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n if (!session.pendingBlocker) throw new Error(`No pending blocker for session ${sessionId}`);\n\n const blocker = session.pendingBlocker;\n session.client.sendUIResponse(blocker.id, { value: response });\n session.pendingBlocker = null;\n if (session.status === 'blocked') {\n session.status = 'running';\n }\n\n this.logger.info('blocker resolved', {\n sessionId,\n projectDir: session.projectDir,\n blockerId: blocker.id,\n blockerMethod: blocker.method,\n });\n }\n\n /**\n * Cancel a running session — abort current operation then stop the process.\n */\n async cancelSession(sessionId: string): Promise<void> {\n const session = this.getSession(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n\n try {\n await session.client.abort();\n } catch { /* may already be stopped */ }\n\n try {\n await session.client.stop();\n } catch { /* swallow */ }\n\n session.status = 'cancelled';\n session.unsubscribe?.();\n session.unsubscribe = undefined;\n\n this.logger.info('session cancelled', { sessionId, projectDir: session.projectDir });\n\n // Symmetric teardown: notify listeners (EventBridge) so they can dispose the\n // session's batcher, channel mappings, and any blocker collectors — the\n // start path emits 'session:started', so cancel must emit too.\n this.emit('session:cancelled', {\n sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n });\n\n this.markTerminal(session.projectDir);\n }\n\n async cancelSessionByDir(projectDir: string): Promise<void> {\n const session = this.getSessionByDir(projectDir);\n if (!session) throw new Error(`Session not found for projectDir: ${projectDir}`);\n await this.cancelSession(session.sessionId);\n }\n\n /**\n * Build a HeadlessJsonResult-shaped object from accumulated session state.\n */\n getResult(sessionId: string): Record<string, unknown> {\n const session = this.getSession(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n\n const durationMs = Date.now() - session.startTime;\n\n return {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n status: session.status,\n durationMs,\n cost: session.cost,\n recentEvents: session.events.slice(-10),\n pendingBlocker: session.pendingBlocker\n ? { id: session.pendingBlocker.id, method: session.pendingBlocker.method, message: session.pendingBlocker.message }\n : null,\n error: session.error ?? null,\n };\n }\n\n /**\n * Stop all active sessions and clean up resources.\n */\n async cleanup(): Promise<void> {\n const stopPromises: Promise<void>[] = [];\n\n for (const session of this.sessions.values()) {\n session.unsubscribe?.();\n if (session.status === 'running' || session.status === 'starting' || session.status === 'blocked') {\n stopPromises.push(\n session.client.stop().catch(() => { /* swallow */ })\n );\n session.status = 'cancelled';\n }\n }\n\n await Promise.allSettled(stopPromises);\n\n this.sessions.clear();\n this.terminalOrder = [];\n }\n\n // ---------------------------------------------------------------------------\n // Private: Terminal-session retention (bounded)\n // ---------------------------------------------------------------------------\n\n /**\n * Record a session (by resolved dir) as terminal and evict the oldest\n * terminal sessions once the retention cap is exceeded. Retains recent\n * terminal sessions for inspection while bounding heap growth.\n */\n private markTerminal(resolvedDir: string): void {\n // Move to most-recent position (dedupe if it was already terminal).\n const existingIdx = this.terminalOrder.indexOf(resolvedDir);\n if (existingIdx !== -1) this.terminalOrder.splice(existingIdx, 1);\n this.terminalOrder.push(resolvedDir);\n\n while (this.terminalOrder.length > MAX_TERMINAL_SESSIONS) {\n const oldest = this.terminalOrder.shift();\n if (oldest) this.evictSession(oldest);\n }\n }\n\n /**\n * Remove a session from tracking and best-effort reclaim its agent process.\n */\n private evictSession(resolvedDir: string): void {\n const session = this.sessions.get(resolvedDir);\n this.sessions.delete(resolvedDir);\n const orderIdx = this.terminalOrder.indexOf(resolvedDir);\n if (orderIdx !== -1) this.terminalOrder.splice(orderIdx, 1);\n\n if (session) {\n session.unsubscribe?.();\n session.unsubscribe = undefined;\n // The agent child process may still be alive (e.g. a completed session\n // kept for follow-up relay) — reclaim it on eviction.\n void session.client.stop().catch(() => { /* swallow */ });\n }\n }\n\n /**\n * Resolve the GSD CLI path.\n *\n * 1. GSD_CLI_PATH env var (highest priority)\n * 2. `which gsd` → resolve to the actual dist/cli.js\n */\n static resolveCLIPath(): string {\n const envPath = process.env['GSD_CLI_PATH'];\n if (envPath) return resolve(envPath);\n\n try {\n const gsdBin = execSync('which gsd', { encoding: 'utf-8' }).trim();\n if (gsdBin) return resolve(gsdBin);\n } catch {\n // which failed\n }\n\n throw new Error(\n 'Cannot find GSD CLI. Set GSD_CLI_PATH environment variable or ensure `gsd` is in PATH.'\n );\n }\n\n // ---------------------------------------------------------------------------\n // Private: Event Handling\n // ---------------------------------------------------------------------------\n\n private handleEvent(session: ManagedSession, event: SdkAgentEvent): void {\n // Ring buffer: push and trim\n session.events.push(event);\n if (session.events.length > MAX_EVENTS) {\n session.events.splice(0, session.events.length - MAX_EVENTS);\n }\n\n // Forward event to listeners\n this.logger.debug('session event', { sessionId: session.sessionId, type: (event as Record<string, unknown>).type as string });\n this.emit('session:event', { sessionId: session.sessionId, projectDir: session.projectDir, event });\n\n // Cost tracking (K004 — cumulative-max)\n if ((event as Record<string, unknown>).type === 'cost_update') {\n const costEvent = event as unknown as RpcCostUpdateEvent;\n session.cost.totalCost = Math.max(session.cost.totalCost, costEvent.cumulativeCost ?? 0);\n if (costEvent.tokens) {\n session.cost.tokens.input = Math.max(session.cost.tokens.input, costEvent.tokens.input ?? 0);\n session.cost.tokens.output = Math.max(session.cost.tokens.output, costEvent.tokens.output ?? 0);\n session.cost.tokens.cacheRead = Math.max(session.cost.tokens.cacheRead, costEvent.tokens.cacheRead ?? 0);\n session.cost.tokens.cacheWrite = Math.max(session.cost.tokens.cacheWrite, costEvent.tokens.cacheWrite ?? 0);\n }\n }\n\n // Terminal detection — auto-mode/step-mode stopped\n if (isTerminalNotification(event as Record<string, unknown>)) {\n if (isBlockedNotification(event as Record<string, unknown>)) {\n session.status = 'blocked';\n session.pendingBlocker = extractBlocker(event);\n this.logger.info('session blocked', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n blockerId: session.pendingBlocker.id,\n blockerMethod: session.pendingBlocker.method,\n });\n this.emit('session:blocked', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n blocker: session.pendingBlocker,\n });\n } else {\n session.status = 'completed';\n session.unsubscribe?.();\n session.unsubscribe = undefined;\n this.logger.info('session completed', { sessionId: session.sessionId, projectDir: session.projectDir });\n this.emit('session:completed', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n });\n this.markTerminal(session.projectDir);\n }\n return;\n }\n\n // Blocker detection — non-fire-and-forget extension_ui_request\n if (isBlockingUIRequest(event as Record<string, unknown>)) {\n session.status = 'blocked';\n session.pendingBlocker = extractBlocker(event);\n this.logger.info('session blocked', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n blockerId: session.pendingBlocker.id,\n blockerMethod: session.pendingBlocker.method,\n });\n this.emit('session:blocked', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n blocker: session.pendingBlocker,\n });\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction timeout(ms: number, message: string): Promise<never> {\n return new Promise((_, reject) => {\n setTimeout(() => reject(new Error(message)), ms);\n });\n}\n\nfunction extractBlocker(event: SdkAgentEvent): PendingBlocker {\n const uiEvent = event as unknown as RpcExtensionUIRequest;\n return {\n id: String(uiEvent.id ?? ''),\n method: uiEvent.method,\n message: String((uiEvent as Record<string, unknown>).title ?? (uiEvent as Record<string, unknown>).message ?? ''),\n event: uiEvent,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-manager.js","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SessionManager — manages RpcClient lifecycle for daemon-driven GSD execution.\n *\n * Extends EventEmitter to emit typed session lifecycle events.\n * One active session per projectDir. Tracks events in a ring buffer,\n * detects blockers, tracks terminal state, and accumulates cost using\n * the cumulative-max pattern (K004).\n *\n * Adapted from packages/mcp-server/src/session-manager.ts with:\n * - Logger integration for structured logging\n * - EventEmitter for session lifecycle events\n * - getAllSessions() for cross-project status (R035)\n * - projectName field on ManagedSession\n */\n\nimport { execSync } from 'node:child_process';\nimport { basename, resolve } from 'node:path';\nimport { EventEmitter } from 'node:events';\nimport { RpcClient } from '@opengsd/rpc-client';\nimport type { RpcCostUpdateEvent, RpcExtensionUIRequest, RpcInitResult, SdkAgentEvent } from '@opengsd/contracts';\nimport type {\n ManagedSession,\n StartSessionOptions,\n PendingBlocker,\n} from './types.js';\nimport { MAX_EVENTS, INIT_TIMEOUT_MS } from './types.js';\nimport type { Logger } from './logger.js';\n\n// ---------------------------------------------------------------------------\n// Inlined detection logic (from headless-events.ts — no internal package imports)\n// ---------------------------------------------------------------------------\n\nconst FIRE_AND_FORGET_METHODS = new Set([\n 'notify', 'setStatus', 'setWidget', 'setTitle', 'set_editor_text',\n]);\n\nconst TERMINAL_PREFIXES = [\n 'auto-mode stopped',\n 'step-mode stopped',\n 'auto-mode complete',\n 'no active milestone',\n 'auto-mode idle',\n];\n\nfunction isTerminalNotification(event: Record<string, unknown>): boolean {\n if (event.type !== 'extension_ui_request' || event.method !== 'notify') return false;\n const message = String(event.message ?? '').toLowerCase();\n return TERMINAL_PREFIXES.some((prefix) => message.startsWith(prefix));\n}\n\nfunction isBlockedNotification(event: Record<string, unknown>): boolean {\n if (event.type !== 'extension_ui_request' || event.method !== 'notify') return false;\n const message = String(event.message ?? '').toLowerCase();\n return message.includes('blocked:');\n}\n\nfunction isBlockingUIRequest(event: Record<string, unknown>): boolean {\n if (event.type !== 'extension_ui_request') return false;\n const method = String(event.method ?? '');\n return !FIRE_AND_FORGET_METHODS.has(method);\n}\n\n// ---------------------------------------------------------------------------\n// SessionManager\n// ---------------------------------------------------------------------------\n\n/** Terminal lifecycle states — work is finished, process may be reclaimed. */\nconst TERMINAL_STATUSES: ReadonlySet<string> = new Set(['completed', 'error', 'cancelled']);\n\n/**\n * How many terminal (completed/cancelled/errored) sessions to retain for\n * inspection before evicting the oldest. Bounds heap growth on a daemon that\n * services many distinct project dirs over its lifetime — without this, every\n * session ever run stays resident forever (events buffer + RpcClient ref).\n */\nconst MAX_TERMINAL_SESSIONS = 50;\n\nexport class SessionManager extends EventEmitter {\n /** Sessions keyed by resolved projectDir for duplicate-start prevention */\n private sessions = new Map<string, ManagedSession>();\n\n /** Resolved dirs of terminal sessions, oldest-first — for bounded retention. */\n private terminalOrder: string[] = [];\n\n constructor(private readonly logger: Logger) {\n super();\n }\n\n /**\n * Start a new GSD auto-mode session for the given project directory.\n *\n * Rejects if a session already exists for this projectDir.\n * Creates an RpcClient, starts the process, performs the v2 init handshake,\n * wires event tracking, and sends '/gsd auto' to begin execution.\n */\n async startSession(options: StartSessionOptions): Promise<string> {\n const { projectDir } = options;\n\n if (!projectDir || projectDir.trim() === '') {\n throw new Error('projectDir is required and cannot be empty');\n }\n\n const resolvedDir = resolve(projectDir);\n const projectName = basename(resolvedDir);\n\n const existing = this.sessions.get(resolvedDir);\n if (existing) {\n // A terminal session (completed/cancelled/error) is retained only for\n // inspection — it must not block a fresh start for the same dir. Evict it\n // and reclaim its process before starting anew.\n if (TERMINAL_STATUSES.has(existing.status)) {\n this.evictSession(resolvedDir);\n } else {\n throw new Error(\n `Session already active for ${resolvedDir} (sessionId: ${existing.sessionId}, status: ${existing.status})`\n );\n }\n }\n\n const cliPath = options.cliPath ?? SessionManager.resolveCLIPath();\n\n const args: string[] = [];\n if (options.model) args.push('--model', options.model);\n if (options.bare) args.push('--bare');\n\n const client = new RpcClient({\n cliPath,\n cwd: resolvedDir,\n args,\n });\n\n // Build the session shell before async operations so we can track state\n const session: ManagedSession = {\n sessionId: '', // filled after init\n projectDir: resolvedDir,\n projectName,\n status: 'starting',\n client,\n events: [],\n pendingBlocker: null,\n cost: { totalCost: 0, tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } },\n startTime: Date.now(),\n };\n\n // Insert into map early (keyed by dir) so concurrent starts are rejected\n this.sessions.set(resolvedDir, session);\n\n try {\n // Start the process with timeout\n await Promise.race([\n client.start(),\n timeout(INIT_TIMEOUT_MS, `RpcClient.start() timed out after ${INIT_TIMEOUT_MS}ms`),\n ]);\n\n // Perform v2 init handshake\n const initResult: RpcInitResult = await Promise.race([\n client.init(),\n timeout(INIT_TIMEOUT_MS, `RpcClient.init() timed out after ${INIT_TIMEOUT_MS}ms`),\n ]) as RpcInitResult;\n\n session.sessionId = initResult.sessionId;\n session.status = 'running';\n\n // Wire event tracking\n session.unsubscribe = client.onEvent((event: SdkAgentEvent) => {\n this.handleEvent(session, event);\n });\n\n // Kick off auto-mode\n const command = options.command ?? '/gsd auto';\n await client.prompt(command);\n\n this.logger.info('session started', { sessionId: session.sessionId, projectDir: resolvedDir });\n this.emit('session:started', { sessionId: session.sessionId, projectDir: resolvedDir, projectName });\n\n return session.sessionId;\n } catch (err) {\n session.status = 'error';\n session.error = err instanceof Error ? err.message : String(err);\n\n // Attempt cleanup\n try { await client.stop(); } catch { /* swallow cleanup errors */ }\n\n this.logger.error('session error', { sessionId: session.sessionId, projectDir: resolvedDir, error: session.error });\n this.emit('session:error', { sessionId: session.sessionId, projectDir: resolvedDir, projectName, error: session.error });\n\n // Keep session in map so callers can inspect the error, but under the\n // bounded terminal-retention policy so it is eventually reclaimed.\n this.markTerminal(resolvedDir);\n throw new Error(`Failed to start session for ${resolvedDir}: ${session.error}`);\n }\n }\n\n /**\n * Look up a session by sessionId.\n * Linear scan is fine — we expect <10 concurrent sessions.\n */\n getSession(sessionId: string): ManagedSession | undefined {\n for (const session of this.sessions.values()) {\n if (session.sessionId === sessionId) return session;\n }\n return undefined;\n }\n\n /**\n * Look up a session by project directory (direct map lookup).\n */\n getSessionByDir(projectDir: string): ManagedSession | undefined {\n return this.sessions.get(resolve(projectDir));\n }\n\n /**\n * Return all tracked sessions (R035 — cross-project status).\n */\n getAllSessions(): ManagedSession[] {\n return Array.from(this.sessions.values());\n }\n\n /**\n * Resolve a pending blocker by sending a UI response.\n */\n async resolveBlocker(sessionId: string, response: string): Promise<void> {\n const session = this.getSession(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n if (!session.pendingBlocker) throw new Error(`No pending blocker for session ${sessionId}`);\n\n const blocker = session.pendingBlocker;\n session.client.sendUIResponse(blocker.id, { value: response });\n session.pendingBlocker = null;\n if (session.status === 'blocked') {\n session.status = 'running';\n }\n\n this.logger.info('blocker resolved', {\n sessionId,\n projectDir: session.projectDir,\n blockerId: blocker.id,\n blockerMethod: blocker.method,\n });\n }\n\n /**\n * Cancel a running session — abort current operation then stop the process.\n */\n async cancelSession(sessionId: string): Promise<void> {\n const session = this.getSession(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n\n try {\n await session.client.abort();\n } catch { /* may already be stopped */ }\n\n try {\n await session.client.stop();\n } catch { /* swallow */ }\n\n session.status = 'cancelled';\n session.unsubscribe?.();\n session.unsubscribe = undefined;\n\n this.logger.info('session cancelled', { sessionId, projectDir: session.projectDir });\n\n // Symmetric teardown: notify listeners (EventBridge) so they can dispose the\n // session's batcher, channel mappings, and any blocker collectors — the\n // start path emits 'session:started', so cancel must emit too.\n this.emit('session:cancelled', {\n sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n });\n\n this.markTerminal(session.projectDir);\n }\n\n async cancelSessionByDir(projectDir: string): Promise<void> {\n const session = this.getSessionByDir(projectDir);\n if (!session) throw new Error(`Session not found for projectDir: ${projectDir}`);\n await this.cancelSession(session.sessionId);\n }\n\n /**\n * Build a HeadlessJsonResult-shaped object from accumulated session state.\n */\n getResult(sessionId: string): Record<string, unknown> {\n const session = this.getSession(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n\n const durationMs = Date.now() - session.startTime;\n\n return {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n status: session.status,\n durationMs,\n cost: session.cost,\n recentEvents: session.events.slice(-10),\n pendingBlocker: session.pendingBlocker\n ? { id: session.pendingBlocker.id, method: session.pendingBlocker.method, message: session.pendingBlocker.message }\n : null,\n error: session.error ?? null,\n };\n }\n\n /**\n * Stop all active sessions and clean up resources.\n */\n async cleanup(): Promise<void> {\n const stopPromises: Promise<void>[] = [];\n\n for (const session of this.sessions.values()) {\n session.unsubscribe?.();\n if (session.status === 'running' || session.status === 'starting' || session.status === 'blocked') {\n stopPromises.push(\n session.client.stop().catch(() => { /* swallow */ })\n );\n session.status = 'cancelled';\n }\n }\n\n await Promise.allSettled(stopPromises);\n\n this.sessions.clear();\n this.terminalOrder = [];\n }\n\n // ---------------------------------------------------------------------------\n // Private: Terminal-session retention (bounded)\n // ---------------------------------------------------------------------------\n\n /**\n * Record a session (by resolved dir) as terminal and evict the oldest\n * terminal sessions once the retention cap is exceeded. Retains recent\n * terminal sessions for inspection while bounding heap growth.\n */\n private markTerminal(resolvedDir: string): void {\n // Move to most-recent position (dedupe if it was already terminal).\n const existingIdx = this.terminalOrder.indexOf(resolvedDir);\n if (existingIdx !== -1) this.terminalOrder.splice(existingIdx, 1);\n this.terminalOrder.push(resolvedDir);\n\n while (this.terminalOrder.length > MAX_TERMINAL_SESSIONS) {\n const oldest = this.terminalOrder.shift();\n if (oldest) this.evictSession(oldest);\n }\n }\n\n /**\n * Remove a session from tracking and best-effort reclaim its agent process.\n */\n private evictSession(resolvedDir: string): void {\n const session = this.sessions.get(resolvedDir);\n this.sessions.delete(resolvedDir);\n const orderIdx = this.terminalOrder.indexOf(resolvedDir);\n if (orderIdx !== -1) this.terminalOrder.splice(orderIdx, 1);\n\n if (session) {\n session.unsubscribe?.();\n session.unsubscribe = undefined;\n // The agent child process may still be alive (e.g. a completed session\n // kept for follow-up relay) — reclaim it on eviction.\n void session.client.stop().catch(() => { /* swallow */ });\n }\n }\n\n /**\n * Resolve the GSD CLI path.\n *\n * 1. GSD_CLI_PATH env var (highest priority)\n * 2. `which gsd` → resolve to the actual dist/cli.js\n */\n static resolveCLIPath(): string {\n const envPath = process.env['GSD_CLI_PATH'];\n if (envPath) return resolve(envPath);\n\n try {\n const gsdBin = execSync('which gsd', { encoding: 'utf-8' }).trim();\n if (gsdBin) return resolve(gsdBin);\n } catch {\n // which failed\n }\n\n throw new Error(\n 'Cannot find GSD CLI. Set GSD_CLI_PATH environment variable or ensure `gsd` is in PATH.'\n );\n }\n\n // ---------------------------------------------------------------------------\n // Private: Event Handling\n // ---------------------------------------------------------------------------\n\n private handleEvent(session: ManagedSession, event: SdkAgentEvent): void {\n // Ring buffer: push and trim\n session.events.push(event);\n if (session.events.length > MAX_EVENTS) {\n session.events.splice(0, session.events.length - MAX_EVENTS);\n }\n\n // Forward event to listeners\n this.logger.debug('session event', { sessionId: session.sessionId, type: (event as Record<string, unknown>).type as string });\n this.emit('session:event', { sessionId: session.sessionId, projectDir: session.projectDir, event });\n\n // Cost tracking (K004 — cumulative-max)\n if ((event as Record<string, unknown>).type === 'cost_update') {\n const costEvent = event as unknown as RpcCostUpdateEvent;\n session.cost.totalCost = Math.max(session.cost.totalCost, costEvent.cumulativeCost ?? 0);\n if (costEvent.tokens) {\n session.cost.tokens.input = Math.max(session.cost.tokens.input, costEvent.tokens.input ?? 0);\n session.cost.tokens.output = Math.max(session.cost.tokens.output, costEvent.tokens.output ?? 0);\n session.cost.tokens.cacheRead = Math.max(session.cost.tokens.cacheRead, costEvent.tokens.cacheRead ?? 0);\n session.cost.tokens.cacheWrite = Math.max(session.cost.tokens.cacheWrite, costEvent.tokens.cacheWrite ?? 0);\n }\n }\n\n // Terminal detection — auto-mode/step-mode stopped\n if (isTerminalNotification(event as Record<string, unknown>)) {\n if (isBlockedNotification(event as Record<string, unknown>)) {\n session.status = 'blocked';\n session.pendingBlocker = extractBlocker(event);\n this.logger.info('session blocked', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n blockerId: session.pendingBlocker.id,\n blockerMethod: session.pendingBlocker.method,\n });\n this.emit('session:blocked', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n blocker: session.pendingBlocker,\n });\n } else {\n session.status = 'completed';\n session.unsubscribe?.();\n session.unsubscribe = undefined;\n this.logger.info('session completed', { sessionId: session.sessionId, projectDir: session.projectDir });\n this.emit('session:completed', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n });\n this.markTerminal(session.projectDir);\n }\n return;\n }\n\n // Blocker detection — non-fire-and-forget extension_ui_request\n if (isBlockingUIRequest(event as Record<string, unknown>)) {\n session.status = 'blocked';\n session.pendingBlocker = extractBlocker(event);\n this.logger.info('session blocked', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n blockerId: session.pendingBlocker.id,\n blockerMethod: session.pendingBlocker.method,\n });\n this.emit('session:blocked', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n blocker: session.pendingBlocker,\n });\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction timeout(ms: number, message: string): Promise<never> {\n return new Promise((_, reject) => {\n setTimeout(() => reject(new Error(message)), ms);\n });\n}\n\nfunction extractBlocker(event: SdkAgentEvent): PendingBlocker {\n const uiEvent = event as unknown as RpcExtensionUIRequest;\n return {\n id: String(uiEvent.id ?? ''),\n method: uiEvent.method,\n message: String((uiEvent as Record<string, unknown>).title ?? (uiEvent as Record<string, unknown>).message ?? ''),\n event: uiEvent,\n };\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengsd/daemon",
|
|
3
|
-
"version": "1.4.0-dev.
|
|
3
|
+
"version": "1.4.0-dev.c18009cd",
|
|
4
4
|
"description": "GSD daemon — background process for project monitoring and Discord integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@anthropic-ai/sdk": "^0.52.0",
|
|
34
|
-
"@opengsd/contracts": "^1.4.0-dev.
|
|
35
|
-
"@opengsd/mcp-server": "^1.4.0-dev.
|
|
36
|
-
"@opengsd/rpc-client": "^1.4.0-dev.
|
|
34
|
+
"@opengsd/contracts": "^1.4.0-dev.c18009cd",
|
|
35
|
+
"@opengsd/mcp-server": "^1.4.0-dev.c18009cd",
|
|
36
|
+
"@opengsd/rpc-client": "^1.4.0-dev.c18009cd",
|
|
37
37
|
"discord.js": "^14.25.1",
|
|
38
38
|
"ws": "^8.20.0",
|
|
39
39
|
"yaml": "^2.8.0",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-session-navigation.d.ts","sourceRoot":"","sources":["../../src/session/agent-session-navigation.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+CAA+C,CAAC;AAOlF,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,8CAA8C,CAAC;AAWvG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,qBAAa,4BAA4B;IAC5B,QAAQ,CAAC,IAAI,EAAE,gBAAgB;gBAAtB,IAAI,EAAE,gBAAgB;IAE3C,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAK5B,qCAAqC,IAAI,OAAO,CAAC,IAAI,CAAC;IAStD,UAAU,CAAC,OAAO,CAAC,EAAE;QAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,KAAK,CAAC,EAAE,CAAC,cAAc,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1D,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,WAAW,CAAC,EAAE,WAAW,CAAC;KAC1B,GAAG,OAAO,CAAC,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-session-navigation.d.ts","sourceRoot":"","sources":["../../src/session/agent-session-navigation.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+CAA+C,CAAC;AAOlF,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,8CAA8C,CAAC;AAWvG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,qBAAa,4BAA4B;IAC5B,QAAQ,CAAC,IAAI,EAAE,gBAAgB;gBAAtB,IAAI,EAAE,gBAAgB;IAE3C,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAK5B,qCAAqC,IAAI,OAAO,CAAC,IAAI,CAAC;IAStD,UAAU,CAAC,OAAO,CAAC,EAAE;QAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,KAAK,CAAC,EAAE,CAAC,cAAc,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1D,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,WAAW,CAAC,EAAE,WAAW,CAAC;KAC1B,GAAG,OAAO,CAAC,OAAO,CAAC;IAoEd,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAkEpD,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;IAmDlF,eAAe,IAAI,MAAM;IAIzB,IAAI,QAAQ,IAAI,UAAU,GAAG,UAAU,CAEtC;IAED,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,GAAG,IAAI;IAI1C,YAAY,CACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO,GAC/G,OAAO,CAAC;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,kBAAkB,CAAA;KAAE,CAAC;IAgK7G,yBAAyB,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAiBrE,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,MAAM;IAWxF,eAAe,IAAI,YAAY;IA6C/B,eAAe,IAAI,YAAY,GAAG,SAAS;IA8CrC,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBxD,aAAa,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM;IAiC1C,oBAAoB,IAAI,MAAM,GAAG,SAAS;CAwB1C"}
|