@cluesmith/codev 2.0.2 → 2.0.6
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/dashboard/dist/assets/index-B-s8BA2l.js +135 -0
- package/dashboard/dist/assets/index-B-s8BA2l.js.map +1 -0
- package/dashboard/dist/assets/index-DB2AxRP7.css +32 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +32 -14
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/architect.d.ts +1 -1
- package/dist/agent-farm/commands/architect.js +3 -3
- package/dist/agent-farm/commands/architect.js.map +1 -1
- package/dist/agent-farm/commands/attach.d.ts +19 -0
- package/dist/agent-farm/commands/attach.d.ts.map +1 -1
- package/dist/agent-farm/commands/attach.js +172 -12
- package/dist/agent-farm/commands/attach.js.map +1 -1
- package/dist/agent-farm/commands/cleanup.js +6 -6
- package/dist/agent-farm/commands/cleanup.js.map +1 -1
- package/dist/agent-farm/commands/open.js +5 -5
- package/dist/agent-farm/commands/open.js.map +1 -1
- package/dist/agent-farm/commands/send.d.ts +22 -2
- package/dist/agent-farm/commands/send.d.ts.map +1 -1
- package/dist/agent-farm/commands/send.js +100 -181
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/shell.js +5 -5
- package/dist/agent-farm/commands/shell.js.map +1 -1
- package/dist/agent-farm/commands/spawn-roles.d.ts +3 -9
- package/dist/agent-farm/commands/spawn-roles.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-roles.js +14 -53
- package/dist/agent-farm/commands/spawn-roles.js.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.d.ts +11 -17
- package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js +32 -13
- package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
- package/dist/agent-farm/commands/spawn.d.ts +8 -6
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +183 -69
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/start.d.ts +4 -4
- package/dist/agent-farm/commands/start.js +16 -16
- package/dist/agent-farm/commands/start.js.map +1 -1
- package/dist/agent-farm/commands/status.d.ts +1 -1
- package/dist/agent-farm/commands/status.d.ts.map +1 -1
- package/dist/agent-farm/commands/status.js +15 -26
- package/dist/agent-farm/commands/status.js.map +1 -1
- package/dist/agent-farm/commands/stop.d.ts +4 -4
- package/dist/agent-farm/commands/stop.js +9 -9
- package/dist/agent-farm/commands/stop.js.map +1 -1
- package/dist/agent-farm/db/index.d.ts.map +1 -1
- package/dist/agent-farm/db/index.js +82 -7
- package/dist/agent-farm/db/index.js.map +1 -1
- package/dist/agent-farm/db/schema.d.ts +2 -2
- package/dist/agent-farm/db/schema.d.ts.map +1 -1
- package/dist/agent-farm/db/schema.js +21 -4
- package/dist/agent-farm/db/schema.js.map +1 -1
- package/dist/agent-farm/lib/tower-client.d.ts +36 -26
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tower-client.js +50 -25
- package/dist/agent-farm/lib/tower-client.js.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.d.ts +12 -2
- package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.js +59 -1
- package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
- package/dist/agent-farm/servers/overview.d.ts +111 -0
- package/dist/agent-farm/servers/overview.d.ts.map +1 -0
- package/dist/agent-farm/servers/overview.js +385 -0
- package/dist/agent-farm/servers/overview.js.map +1 -0
- package/dist/agent-farm/servers/tower-instances.d.ts +18 -20
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-instances.js +97 -100
- package/dist/agent-farm/servers/tower-instances.js.map +1 -1
- package/dist/agent-farm/servers/tower-messages.d.ts +87 -0
- package/dist/agent-farm/servers/tower-messages.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-messages.js +202 -0
- package/dist/agent-farm/servers/tower-messages.js.map +1 -0
- package/dist/agent-farm/servers/tower-routes.d.ts +1 -1
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-routes.js +343 -174
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +50 -21
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts +35 -31
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.js +208 -184
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.d.ts +2 -2
- package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.js +12 -12
- package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
- package/dist/agent-farm/servers/tower-types.d.ts +8 -12
- package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-utils.d.ts +9 -9
- package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-utils.js +18 -18
- package/dist/agent-farm/servers/tower-utils.js.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.d.ts +2 -2
- package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.js +39 -18
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
- package/dist/agent-farm/types.d.ts +5 -6
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/agent-names.d.ts +85 -0
- package/dist/agent-farm/utils/agent-names.d.ts.map +1 -0
- package/dist/agent-farm/utils/agent-names.js +140 -0
- package/dist/agent-farm/utils/agent-names.js.map +1 -0
- package/dist/agent-farm/utils/config.d.ts +1 -1
- package/dist/agent-farm/utils/config.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.js +16 -16
- package/dist/agent-farm/utils/config.js.map +1 -1
- package/dist/agent-farm/utils/file-tabs.d.ts +3 -3
- package/dist/agent-farm/utils/file-tabs.d.ts.map +1 -1
- package/dist/agent-farm/utils/file-tabs.js +9 -9
- package/dist/agent-farm/utils/file-tabs.js.map +1 -1
- package/dist/agent-farm/utils/index.d.ts +0 -1
- package/dist/agent-farm/utils/index.d.ts.map +1 -1
- package/dist/agent-farm/utils/index.js +0 -1
- package/dist/agent-farm/utils/index.js.map +1 -1
- package/dist/agent-farm/utils/message-format.d.ts +17 -0
- package/dist/agent-farm/utils/message-format.d.ts.map +1 -0
- package/dist/agent-farm/utils/message-format.js +41 -0
- package/dist/agent-farm/utils/message-format.js.map +1 -0
- package/dist/agent-farm/utils/notifications.d.ts +4 -4
- package/dist/agent-farm/utils/notifications.d.ts.map +1 -1
- package/dist/agent-farm/utils/notifications.js +18 -18
- package/dist/agent-farm/utils/notifications.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts +2 -2
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +13 -15
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/index.d.ts +26 -2
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +296 -83
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/consult/metrics.d.ts +90 -0
- package/dist/commands/consult/metrics.d.ts.map +1 -0
- package/dist/commands/consult/metrics.js +203 -0
- package/dist/commands/consult/metrics.js.map +1 -0
- package/dist/commands/consult/stats.d.ts +18 -0
- package/dist/commands/consult/stats.d.ts.map +1 -0
- package/dist/commands/consult/stats.js +150 -0
- package/dist/commands/consult/stats.js.map +1 -0
- package/dist/commands/consult/usage-extractor.d.ts +38 -0
- package/dist/commands/consult/usage-extractor.d.ts.map +1 -0
- package/dist/commands/consult/usage-extractor.js +99 -0
- package/dist/commands/consult/usage-extractor.js.map +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +11 -9
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/import.js +4 -4
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +13 -15
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/porch/index.d.ts +6 -6
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +37 -37
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/porch/next.d.ts +1 -1
- package/dist/commands/porch/next.d.ts.map +1 -1
- package/dist/commands/porch/next.js +86 -92
- package/dist/commands/porch/next.js.map +1 -1
- package/dist/commands/porch/notify.d.ts +11 -0
- package/dist/commands/porch/notify.d.ts.map +1 -0
- package/dist/commands/porch/notify.js +30 -0
- package/dist/commands/porch/notify.js.map +1 -0
- package/dist/commands/porch/plan.d.ts +1 -1
- package/dist/commands/porch/plan.d.ts.map +1 -1
- package/dist/commands/porch/plan.js +3 -3
- package/dist/commands/porch/plan.js.map +1 -1
- package/dist/commands/porch/prompts.d.ts +10 -1
- package/dist/commands/porch/prompts.d.ts.map +1 -1
- package/dist/commands/porch/prompts.js +59 -35
- package/dist/commands/porch/prompts.js.map +1 -1
- package/dist/commands/porch/protocol.d.ts +1 -1
- package/dist/commands/porch/protocol.d.ts.map +1 -1
- package/dist/commands/porch/protocol.js +8 -8
- package/dist/commands/porch/protocol.js.map +1 -1
- package/dist/commands/porch/state.d.ts +6 -6
- package/dist/commands/porch/state.d.ts.map +1 -1
- package/dist/commands/porch/state.js +14 -12
- package/dist/commands/porch/state.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +10 -11
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/github.d.ts +81 -0
- package/dist/lib/github.d.ts.map +1 -0
- package/dist/lib/github.js +141 -0
- package/dist/lib/github.js.map +1 -0
- package/dist/lib/scaffold.d.ts +13 -21
- package/dist/lib/scaffold.d.ts.map +1 -1
- package/dist/lib/scaffold.js +34 -57
- package/dist/lib/scaffold.js.map +1 -1
- package/dist/lib/skeleton.d.ts +7 -7
- package/dist/lib/skeleton.d.ts.map +1 -1
- package/dist/lib/skeleton.js +10 -10
- package/dist/lib/skeleton.js.map +1 -1
- package/dist/terminal/index.d.ts +14 -0
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +12 -0
- package/dist/terminal/index.js.map +1 -1
- package/dist/terminal/pty-manager.d.ts +1 -1
- package/dist/terminal/pty-manager.d.ts.map +1 -1
- package/dist/terminal/pty-manager.js +10 -7
- package/dist/terminal/pty-manager.js.map +1 -1
- package/dist/terminal/pty-session.js +3 -3
- package/dist/terminal/pty-session.js.map +1 -1
- package/dist/terminal/session-manager.d.ts +64 -0
- package/dist/terminal/session-manager.d.ts.map +1 -1
- package/dist/terminal/session-manager.js +299 -10
- package/dist/terminal/session-manager.js.map +1 -1
- package/dist/terminal/shellper-client.d.ts +2 -1
- package/dist/terminal/shellper-client.d.ts.map +1 -1
- package/dist/terminal/shellper-client.js +4 -2
- package/dist/terminal/shellper-client.js.map +1 -1
- package/dist/terminal/shellper-main.js +33 -4
- package/dist/terminal/shellper-main.js.map +1 -1
- package/dist/terminal/shellper-process.d.ts +24 -7
- package/dist/terminal/shellper-process.d.ts.map +1 -1
- package/dist/terminal/shellper-process.js +139 -36
- package/dist/terminal/shellper-process.js.map +1 -1
- package/dist/terminal/shellper-protocol.d.ts +1 -0
- package/dist/terminal/shellper-protocol.d.ts.map +1 -1
- package/dist/terminal/shellper-protocol.js.map +1 -1
- package/package.json +4 -1
- package/skeleton/.claude/skills/af/SKILL.md +7 -7
- package/skeleton/.claude/skills/consult/SKILL.md +1 -1
- package/skeleton/builders.md +2 -2
- package/skeleton/maintain/.gitkeep +1 -1
- package/skeleton/porch/prompts/specify.md +1 -1
- package/skeleton/protocols/bugfix/prompts/pr.md +15 -4
- package/skeleton/protocols/experiment/protocol.md +17 -17
- package/skeleton/protocols/maintain/prompts/audit.md +2 -2
- package/skeleton/protocols/maintain/prompts/sync.md +1 -1
- package/skeleton/protocols/maintain/prompts/verify.md +1 -1
- package/skeleton/protocols/maintain/protocol.md +8 -9
- package/skeleton/protocols/maintain/templates/maintenance-run.md +2 -2
- package/skeleton/protocols/spir/protocol.json +5 -5
- package/skeleton/protocols/spir/protocol.md +8 -8
- package/skeleton/protocols/tick/protocol.md +31 -31
- package/skeleton/resources/commands/agent-farm.md +14 -14
- package/skeleton/resources/commands/codev.md +0 -1
- package/skeleton/resources/commands/consult.md +3 -3
- package/skeleton/resources/spikes.md +3 -3
- package/skeleton/resources/workflow-reference.md +14 -14
- package/skeleton/roles/architect.md +25 -25
- package/skeleton/roles/builder.md +1 -1
- package/skeleton/roles/consultant.md +6 -0
- package/skeleton/templates/AGENTS.md +5 -5
- package/skeleton/templates/CLAUDE.md +5 -5
- package/skeleton/templates/lifecycle.md +9 -9
- package/templates/open.html +19 -16
- package/templates/tower.html +54 -94
- package/templates/vendor/marked.min.js +6 -0
- package/templates/vendor/prism-bash.min.js +1 -0
- package/templates/vendor/prism-css.min.js +1 -0
- package/templates/vendor/prism-javascript.min.js +1 -0
- package/templates/vendor/prism-json.min.js +1 -0
- package/templates/vendor/prism-markdown.min.js +1 -0
- package/templates/vendor/prism-markup.min.js +1 -0
- package/templates/vendor/prism-python.min.js +1 -0
- package/templates/vendor/prism-tomorrow.min.css +1 -0
- package/templates/vendor/prism-typescript.min.js +1 -0
- package/templates/vendor/prism-yaml.min.js +1 -0
- package/templates/vendor/prism.min.js +1 -0
- package/templates/vendor/purify.min.js +3 -0
- package/dashboard/dist/assets/index-4n9zpWLY.css +0 -32
- package/dashboard/dist/assets/index-b38SaXk5.js +0 -136
- package/dashboard/dist/assets/index-b38SaXk5.js.map +0 -1
- package/dist/agent-farm/hq-connector.d.ts +0 -19
- package/dist/agent-farm/hq-connector.d.ts.map +0 -1
- package/dist/agent-farm/hq-connector.js +0 -351
- package/dist/agent-farm/hq-connector.js.map +0 -1
- package/dist/agent-farm/utils/deps.d.ts +0 -51
- package/dist/agent-farm/utils/deps.d.ts.map +0 -1
- package/dist/agent-farm/utils/deps.js +0 -162
- package/dist/agent-farm/utils/deps.js.map +0 -1
- package/dist/agent-farm/utils/gate-status.d.ts +0 -16
- package/dist/agent-farm/utils/gate-status.d.ts.map +0 -1
- package/dist/agent-farm/utils/gate-status.js +0 -79
- package/dist/agent-farm/utils/gate-status.js.map +0 -1
- package/dist/agent-farm/utils/gate-watcher.d.ts +0 -38
- package/dist/agent-farm/utils/gate-watcher.d.ts.map +0 -1
- package/dist/agent-farm/utils/gate-watcher.js +0 -122
- package/dist/agent-farm/utils/gate-watcher.js.map +0 -1
- package/dist/agent-farm/utils/session.d.ts +0 -32
- package/dist/agent-farm/utils/session.d.ts.map +0 -1
- package/dist/agent-farm/utils/session.js +0 -57
- package/dist/agent-farm/utils/session.js.map +0 -1
- package/dist/lib/projectlist-parser.d.ts +0 -70
- package/dist/lib/projectlist-parser.d.ts.map +0 -1
- package/dist/lib/projectlist-parser.js +0 -200
- package/dist/lib/projectlist-parser.js.map +0 -1
- package/skeleton/templates/projectlist-archive.md +0 -21
- package/skeleton/templates/projectlist.md +0 -147
- package/templates/dashboard/css/dialogs.css +0 -149
- package/templates/dashboard/css/files.css +0 -558
- package/templates/dashboard/css/layout.css +0 -133
- package/templates/dashboard/css/projects.css +0 -501
- package/templates/dashboard/css/statusbar.css +0 -23
- package/templates/dashboard/css/tabs.css +0 -314
- package/templates/dashboard/css/utilities.css +0 -50
- package/templates/dashboard/css/variables.css +0 -45
- package/templates/dashboard/index.html +0 -149
- package/templates/dashboard/js/dialogs.js +0 -368
- package/templates/dashboard/js/files.js +0 -448
- package/templates/dashboard/js/main.js +0 -476
- package/templates/dashboard/js/projects.js +0 -544
- package/templates/dashboard/js/state.js +0 -91
- package/templates/dashboard/js/tabs.js +0 -518
- package/templates/dashboard/js/utils.js +0 -191
|
@@ -3,31 +3,25 @@
|
|
|
3
3
|
* Spec 0105: Tower Server Decomposition — Phase 4
|
|
4
4
|
*
|
|
5
5
|
* Contains: terminal session CRUD, file tab persistence, shell ID allocation,
|
|
6
|
-
* terminal reconciliation,
|
|
6
|
+
* terminal reconciliation, and terminal list assembly.
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { getGlobalDb } from '../db/index.js';
|
|
12
|
-
import {
|
|
13
|
-
import { GateWatcher } from '../utils/gate-watcher.js';
|
|
14
|
-
import { saveFileTab as saveFileTabToDb, deleteFileTab as deleteFileTabFromDb, loadFileTabsForProject as loadFileTabsFromDb, } from '../utils/file-tabs.js';
|
|
12
|
+
import { saveFileTab as saveFileTabToDb, deleteFileTab as deleteFileTabFromDb, loadFileTabsForWorkspace as loadFileTabsFromDb, } from '../utils/file-tabs.js';
|
|
15
13
|
import { TerminalManager } from '../../terminal/pty-manager.js';
|
|
16
|
-
import {
|
|
14
|
+
import { normalizeWorkspacePath, buildArchitectArgs } from './tower-utils.js';
|
|
17
15
|
// ============================================================================
|
|
18
16
|
// Module-private state (lifecycle driven by orchestrator)
|
|
19
17
|
// ============================================================================
|
|
20
18
|
let _deps = null;
|
|
21
|
-
/**
|
|
22
|
-
const
|
|
19
|
+
/** Workspace terminal registry — tracks which terminals belong to which workspace */
|
|
20
|
+
const workspaceTerminals = new Map();
|
|
23
21
|
/** Global TerminalManager instance (lazy singleton) */
|
|
24
22
|
let terminalManager = null;
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
if (_deps)
|
|
28
|
-
_deps.log(...args);
|
|
29
|
-
});
|
|
30
|
-
let gateWatcherInterval = null;
|
|
23
|
+
/** True while reconcileTerminalSessions() is running — blocks on-the-fly reconnection (Bugfix #274) */
|
|
24
|
+
let _reconciling = false;
|
|
31
25
|
// ============================================================================
|
|
32
26
|
// Lifecycle
|
|
33
27
|
// ============================================================================
|
|
@@ -35,12 +29,12 @@ let gateWatcherInterval = null;
|
|
|
35
29
|
export function initTerminals(deps) {
|
|
36
30
|
_deps = deps;
|
|
37
31
|
}
|
|
32
|
+
/** Check if reconciliation is currently in progress (Bugfix #274) */
|
|
33
|
+
export function isReconciling() {
|
|
34
|
+
return _reconciling;
|
|
35
|
+
}
|
|
38
36
|
/** Tear down the terminal module */
|
|
39
37
|
export function shutdownTerminals() {
|
|
40
|
-
if (gateWatcherInterval) {
|
|
41
|
-
clearInterval(gateWatcherInterval);
|
|
42
|
-
gateWatcherInterval = null;
|
|
43
|
-
}
|
|
44
38
|
if (terminalManager) {
|
|
45
39
|
terminalManager.shutdown();
|
|
46
40
|
terminalManager = null;
|
|
@@ -50,19 +44,19 @@ export function shutdownTerminals() {
|
|
|
50
44
|
// ============================================================================
|
|
51
45
|
// Accessors for shared state
|
|
52
46
|
// ============================================================================
|
|
53
|
-
/** Get the
|
|
54
|
-
export function
|
|
55
|
-
return
|
|
47
|
+
/** Get the workspace terminals registry (returns the Map reference) */
|
|
48
|
+
export function getWorkspaceTerminals() {
|
|
49
|
+
return workspaceTerminals;
|
|
56
50
|
}
|
|
57
51
|
/**
|
|
58
52
|
* Get or create the global TerminalManager instance.
|
|
59
|
-
* Uses a temporary directory as
|
|
53
|
+
* Uses a temporary directory as workspaceRoot since terminals can be for any workspace.
|
|
60
54
|
*/
|
|
61
55
|
export function getTerminalManager() {
|
|
62
56
|
if (!terminalManager) {
|
|
63
|
-
const
|
|
57
|
+
const workspaceRoot = process.env.HOME || '/tmp';
|
|
64
58
|
terminalManager = new TerminalManager({
|
|
65
|
-
|
|
59
|
+
workspaceRoot: workspaceRoot,
|
|
66
60
|
logDir: path.join(homedir(), '.agent-farm', 'logs'),
|
|
67
61
|
maxSessions: 100,
|
|
68
62
|
ringBufferLines: 10000,
|
|
@@ -77,15 +71,15 @@ export function getTerminalManager() {
|
|
|
77
71
|
// Terminal session CRUD
|
|
78
72
|
// ============================================================================
|
|
79
73
|
/**
|
|
80
|
-
* Get or create
|
|
81
|
-
* On first access for a
|
|
74
|
+
* Get or create workspace terminal registry entry.
|
|
75
|
+
* On first access for a workspace, hydrates file tabs from SQLite so
|
|
82
76
|
* persisted tabs are available immediately (not just after /api/state).
|
|
83
77
|
*/
|
|
84
|
-
export function
|
|
85
|
-
let entry =
|
|
78
|
+
export function getWorkspaceTerminalsEntry(workspacePath) {
|
|
79
|
+
let entry = workspaceTerminals.get(workspacePath);
|
|
86
80
|
if (!entry) {
|
|
87
|
-
entry = { builders: new Map(), shells: new Map(), fileTabs:
|
|
88
|
-
|
|
81
|
+
entry = { builders: new Map(), shells: new Map(), fileTabs: loadFileTabsForWorkspace(workspacePath) };
|
|
82
|
+
workspaceTerminals.set(workspacePath, entry);
|
|
89
83
|
}
|
|
90
84
|
// Migration: ensure fileTabs exists for older entries
|
|
91
85
|
if (!entry.fileTabs) {
|
|
@@ -94,10 +88,10 @@ export function getProjectTerminalsEntry(projectPath) {
|
|
|
94
88
|
return entry;
|
|
95
89
|
}
|
|
96
90
|
/**
|
|
97
|
-
* Generate next shell ID for a
|
|
91
|
+
* Generate next shell ID for a workspace
|
|
98
92
|
*/
|
|
99
|
-
export function getNextShellId(
|
|
100
|
-
const entry =
|
|
93
|
+
export function getNextShellId(workspacePath) {
|
|
94
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
101
95
|
let maxId = 0;
|
|
102
96
|
for (const id of entry.shells.keys()) {
|
|
103
97
|
const num = parseInt(id.replace('shell-', ''), 10);
|
|
@@ -108,20 +102,20 @@ export function getNextShellId(projectPath) {
|
|
|
108
102
|
}
|
|
109
103
|
/**
|
|
110
104
|
* Save a terminal session to SQLite.
|
|
111
|
-
* Guards against race conditions by checking if
|
|
105
|
+
* Guards against race conditions by checking if workspace is still active.
|
|
112
106
|
*/
|
|
113
|
-
export function saveTerminalSession(terminalId,
|
|
107
|
+
export function saveTerminalSession(terminalId, workspacePath, type, roleId, pid, shellperSocket = null, shellperPid = null, shellperStartTime = null) {
|
|
114
108
|
try {
|
|
115
|
-
const normalizedPath =
|
|
116
|
-
// Race condition guard: only save if
|
|
109
|
+
const normalizedPath = normalizeWorkspacePath(workspacePath);
|
|
110
|
+
// Race condition guard: only save if workspace is still in the active registry
|
|
117
111
|
// This prevents zombie rows when stop races with session creation
|
|
118
|
-
if (!
|
|
119
|
-
_deps?.log('INFO', `Skipping session save -
|
|
112
|
+
if (!workspaceTerminals.has(normalizedPath) && !workspaceTerminals.has(workspacePath)) {
|
|
113
|
+
_deps?.log('INFO', `Skipping session save - workspace no longer active: ${workspacePath}`);
|
|
120
114
|
return;
|
|
121
115
|
}
|
|
122
116
|
const db = getGlobalDb();
|
|
123
117
|
db.prepare(`
|
|
124
|
-
INSERT OR REPLACE INTO terminal_sessions (id,
|
|
118
|
+
INSERT OR REPLACE INTO terminal_sessions (id, workspace_path, type, role_id, pid, shellper_socket, shellper_pid, shellper_start_time)
|
|
125
119
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
126
120
|
`).run(terminalId, normalizedPath, type, roleId, pid, shellperSocket, shellperPid, shellperStartTime);
|
|
127
121
|
_deps?.log('INFO', `Saved terminal session to SQLite: ${terminalId} (${type}) for ${path.basename(normalizedPath)}`);
|
|
@@ -150,32 +144,59 @@ export function deleteTerminalSession(terminalId) {
|
|
|
150
144
|
}
|
|
151
145
|
}
|
|
152
146
|
/**
|
|
153
|
-
*
|
|
147
|
+
* Remove a terminal from the in-memory workspace registry.
|
|
148
|
+
* Scans all workspaces to find and remove the terminal by its ID.
|
|
149
|
+
* This is needed when a single terminal is killed (e.g. af cleanup)
|
|
150
|
+
* to keep the in-memory state consistent with SQLite.
|
|
151
|
+
* Bugfix #290: af cleanup didn't remove terminals from in-memory registry.
|
|
152
|
+
*/
|
|
153
|
+
export function removeTerminalFromRegistry(terminalId) {
|
|
154
|
+
for (const [, entry] of workspaceTerminals) {
|
|
155
|
+
if (entry.architect === terminalId) {
|
|
156
|
+
entry.architect = undefined;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
for (const [builderId, tid] of entry.builders) {
|
|
160
|
+
if (tid === terminalId) {
|
|
161
|
+
entry.builders.delete(builderId);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const [shellId, tid] of entry.shells) {
|
|
166
|
+
if (tid === terminalId) {
|
|
167
|
+
entry.shells.delete(shellId);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Delete all terminal sessions for a workspace from SQLite.
|
|
154
175
|
* Normalizes path to ensure consistent cleanup regardless of how path was provided.
|
|
155
176
|
*/
|
|
156
|
-
export function
|
|
177
|
+
export function deleteWorkspaceTerminalSessions(workspacePath) {
|
|
157
178
|
try {
|
|
158
|
-
const normalizedPath =
|
|
179
|
+
const normalizedPath = normalizeWorkspacePath(workspacePath);
|
|
159
180
|
const db = getGlobalDb();
|
|
160
181
|
// Delete both normalized and raw path to handle any inconsistencies
|
|
161
|
-
db.prepare('DELETE FROM terminal_sessions WHERE
|
|
162
|
-
if (normalizedPath !==
|
|
163
|
-
db.prepare('DELETE FROM terminal_sessions WHERE
|
|
182
|
+
db.prepare('DELETE FROM terminal_sessions WHERE workspace_path = ?').run(normalizedPath);
|
|
183
|
+
if (normalizedPath !== workspacePath) {
|
|
184
|
+
db.prepare('DELETE FROM terminal_sessions WHERE workspace_path = ?').run(workspacePath);
|
|
164
185
|
}
|
|
165
186
|
}
|
|
166
187
|
catch (err) {
|
|
167
|
-
_deps?.log('WARN', `Failed to delete
|
|
188
|
+
_deps?.log('WARN', `Failed to delete workspace terminal sessions: ${err.message}`);
|
|
168
189
|
}
|
|
169
190
|
}
|
|
170
191
|
/**
|
|
171
|
-
* Get terminal sessions from SQLite for a
|
|
192
|
+
* Get terminal sessions from SQLite for a workspace.
|
|
172
193
|
* Normalizes path for consistent lookup.
|
|
173
194
|
*/
|
|
174
|
-
export function
|
|
195
|
+
export function getTerminalSessionsForWorkspace(workspacePath) {
|
|
175
196
|
try {
|
|
176
|
-
const normalizedPath =
|
|
197
|
+
const normalizedPath = normalizeWorkspacePath(workspacePath);
|
|
177
198
|
const db = getGlobalDb();
|
|
178
|
-
return db.prepare('SELECT * FROM terminal_sessions WHERE
|
|
199
|
+
return db.prepare('SELECT * FROM terminal_sessions WHERE workspace_path = ?').all(normalizedPath);
|
|
179
200
|
}
|
|
180
201
|
catch {
|
|
181
202
|
return [];
|
|
@@ -188,9 +209,9 @@ export function getTerminalSessionsForProject(projectPath) {
|
|
|
188
209
|
* Save a file tab to SQLite for persistence across Tower restarts.
|
|
189
210
|
* Thin wrapper around utils/file-tabs.ts with error handling and path normalization.
|
|
190
211
|
*/
|
|
191
|
-
export function saveFileTab(id,
|
|
212
|
+
export function saveFileTab(id, workspacePath, filePath, createdAt) {
|
|
192
213
|
try {
|
|
193
|
-
const normalizedPath =
|
|
214
|
+
const normalizedPath = normalizeWorkspacePath(workspacePath);
|
|
194
215
|
saveFileTabToDb(getGlobalDb(), id, normalizedPath, filePath, createdAt);
|
|
195
216
|
}
|
|
196
217
|
catch (err) {
|
|
@@ -210,12 +231,12 @@ export function deleteFileTab(id) {
|
|
|
210
231
|
}
|
|
211
232
|
}
|
|
212
233
|
/**
|
|
213
|
-
* Load file tabs for a
|
|
234
|
+
* Load file tabs for a workspace from SQLite.
|
|
214
235
|
* Thin wrapper around utils/file-tabs.ts with error handling and path normalization.
|
|
215
236
|
*/
|
|
216
|
-
export function
|
|
237
|
+
export function loadFileTabsForWorkspace(workspacePath) {
|
|
217
238
|
try {
|
|
218
|
-
const normalizedPath =
|
|
239
|
+
const normalizedPath = normalizeWorkspacePath(workspacePath);
|
|
219
240
|
return loadFileTabsFromDb(getGlobalDb(), normalizedPath);
|
|
220
241
|
}
|
|
221
242
|
catch (err) {
|
|
@@ -260,6 +281,17 @@ export function processExists(pid) {
|
|
|
260
281
|
export async function reconcileTerminalSessions() {
|
|
261
282
|
if (!_deps)
|
|
262
283
|
return;
|
|
284
|
+
_reconciling = true;
|
|
285
|
+
try {
|
|
286
|
+
await _reconcileTerminalSessionsInner();
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
_reconciling = false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function _reconcileTerminalSessionsInner() {
|
|
293
|
+
if (!_deps)
|
|
294
|
+
return; // Redundant guard for TypeScript narrowing
|
|
263
295
|
const manager = getTerminalManager();
|
|
264
296
|
const db = getGlobalDb();
|
|
265
297
|
let shellperReconnected = 0;
|
|
@@ -281,12 +313,12 @@ export async function reconcileTerminalSessions() {
|
|
|
281
313
|
if (shellperSessions.length > 0) {
|
|
282
314
|
_deps.log('INFO', `Found ${shellperSessions.length} shellper session(s) in SQLite — reconnecting...`);
|
|
283
315
|
}
|
|
316
|
+
const probeTasks = [];
|
|
284
317
|
for (const dbSession of shellperSessions) {
|
|
285
|
-
const
|
|
286
|
-
// Skip sessions whose
|
|
287
|
-
if (!fs.existsSync(
|
|
288
|
-
_deps.log('INFO', `Skipping shellper session ${dbSession.id} —
|
|
289
|
-
// Kill orphaned shellper process before removing row
|
|
318
|
+
const workspacePath = dbSession.workspace_path;
|
|
319
|
+
// Skip sessions whose workspace path doesn't exist or is in temp directory
|
|
320
|
+
if (!fs.existsSync(workspacePath)) {
|
|
321
|
+
_deps.log('INFO', `Skipping shellper session ${dbSession.id} — workspace path no longer exists: ${workspacePath}`);
|
|
290
322
|
if (dbSession.shellper_pid && processExists(dbSession.shellper_pid)) {
|
|
291
323
|
try {
|
|
292
324
|
process.kill(dbSession.shellper_pid, 'SIGTERM');
|
|
@@ -299,9 +331,8 @@ export async function reconcileTerminalSessions() {
|
|
|
299
331
|
continue;
|
|
300
332
|
}
|
|
301
333
|
const tmpDirs = ['/tmp', '/private/tmp', '/var/folders', '/private/var/folders'];
|
|
302
|
-
if (tmpDirs.some(d =>
|
|
303
|
-
_deps.log('INFO', `Skipping shellper session ${dbSession.id} —
|
|
304
|
-
// Kill orphaned shellper process before removing row
|
|
334
|
+
if (tmpDirs.some(d => workspacePath === d || workspacePath.startsWith(d + '/'))) {
|
|
335
|
+
_deps.log('INFO', `Skipping shellper session ${dbSession.id} — workspace is in temp directory: ${workspacePath}`);
|
|
305
336
|
if (dbSession.shellper_pid && processExists(dbSession.shellper_pid)) {
|
|
306
337
|
try {
|
|
307
338
|
process.kill(dbSession.shellper_pid, 'SIGTERM');
|
|
@@ -317,78 +348,98 @@ export async function reconcileTerminalSessions() {
|
|
|
317
348
|
_deps.log('WARN', `Shellper manager not initialized — cannot reconnect ${dbSession.id}`);
|
|
318
349
|
continue;
|
|
319
350
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
architectCmd = config.shell.architect;
|
|
331
|
-
}
|
|
351
|
+
// Build restart options for architect sessions (synchronous, no I/O)
|
|
352
|
+
let restartOptions;
|
|
353
|
+
if (dbSession.type === 'architect') {
|
|
354
|
+
let architectCmd = 'claude';
|
|
355
|
+
const configPath = path.join(workspacePath, 'af-config.json');
|
|
356
|
+
if (fs.existsSync(configPath)) {
|
|
357
|
+
try {
|
|
358
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
359
|
+
if (config.shell?.architect) {
|
|
360
|
+
architectCmd = config.shell.architect;
|
|
332
361
|
}
|
|
333
|
-
catch { /* use default */ }
|
|
334
362
|
}
|
|
335
|
-
|
|
336
|
-
const cleanEnv = { ...process.env };
|
|
337
|
-
delete cleanEnv['CLAUDECODE'];
|
|
338
|
-
restartOptions = {
|
|
339
|
-
command: cmdParts[0],
|
|
340
|
-
args: buildArchitectArgs(cmdParts.slice(1), projectPath),
|
|
341
|
-
cwd: projectPath,
|
|
342
|
-
env: cleanEnv,
|
|
343
|
-
restartDelay: 2000,
|
|
344
|
-
maxRestarts: 50,
|
|
345
|
-
};
|
|
363
|
+
catch { /* use default */ }
|
|
346
364
|
}
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
365
|
+
const cmdParts = architectCmd.split(/\s+/);
|
|
366
|
+
const cleanEnv = { ...process.env };
|
|
367
|
+
delete cleanEnv['CLAUDECODE'];
|
|
368
|
+
restartOptions = {
|
|
369
|
+
command: cmdParts[0],
|
|
370
|
+
args: buildArchitectArgs(cmdParts.slice(1), workspacePath),
|
|
371
|
+
cwd: workspacePath,
|
|
372
|
+
env: cleanEnv,
|
|
373
|
+
restartDelay: 2000,
|
|
374
|
+
maxRestarts: 50,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
probeTasks.push({ dbSession, restartOptions });
|
|
378
|
+
}
|
|
379
|
+
// Probe shellper sockets in parallel with bounded concurrency (Spec 0122 Phase 2)
|
|
380
|
+
const CONCURRENCY_LIMIT = 5;
|
|
381
|
+
const probeResults = [];
|
|
382
|
+
for (let i = 0; i < probeTasks.length; i += CONCURRENCY_LIMIT) {
|
|
383
|
+
const batch = probeTasks.slice(i, i + CONCURRENCY_LIMIT);
|
|
384
|
+
const results = await Promise.allSettled(batch.map(async (task) => {
|
|
385
|
+
const client = await _deps.shellperManager.reconnectSession(task.dbSession.id, task.dbSession.shellper_socket, task.dbSession.shellper_pid, task.dbSession.shellper_start_time, task.restartOptions);
|
|
386
|
+
return { dbSession: task.dbSession, client, restartOptions: task.restartOptions };
|
|
387
|
+
}));
|
|
388
|
+
for (const result of results) {
|
|
389
|
+
if (result.status === 'fulfilled') {
|
|
390
|
+
probeResults.push(result.value);
|
|
370
391
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (ptySession) {
|
|
377
|
-
ptySession.on('exit', () => {
|
|
378
|
-
const currentEntry = getProjectTerminalsEntry(projectPath);
|
|
379
|
-
if (dbSession.type === 'architect' && currentEntry.architect === session.id) {
|
|
380
|
-
currentEntry.architect = undefined;
|
|
381
|
-
}
|
|
382
|
-
deleteTerminalSession(session.id);
|
|
383
|
-
});
|
|
392
|
+
else {
|
|
393
|
+
// Find the corresponding task for error logging
|
|
394
|
+
const idx = results.indexOf(result);
|
|
395
|
+
const task = batch[idx];
|
|
396
|
+
_deps.log('WARN', `Failed to reconnect shellper session ${task.dbSession.id}: ${result.reason?.message ?? result.reason}`);
|
|
384
397
|
}
|
|
385
|
-
matchedSessionIds.add(dbSession.id);
|
|
386
|
-
shellperReconnected++;
|
|
387
|
-
_deps.log('INFO', `Reconnected shellper session → ${session.id} (${dbSession.type} for ${path.basename(projectPath)})`);
|
|
388
398
|
}
|
|
389
|
-
|
|
390
|
-
|
|
399
|
+
}
|
|
400
|
+
// Process probe results sequentially (shared state mutations)
|
|
401
|
+
for (const { dbSession, client } of probeResults) {
|
|
402
|
+
if (!client) {
|
|
403
|
+
_deps.log('INFO', `Shellper session ${dbSession.id} is stale (PID/socket dead) — will clean up`);
|
|
404
|
+
continue; // Will be cleaned up in Phase 2
|
|
405
|
+
}
|
|
406
|
+
const workspacePath = dbSession.workspace_path;
|
|
407
|
+
const replayData = client.getReplayData() ?? Buffer.alloc(0);
|
|
408
|
+
const label = dbSession.type === 'architect' ? 'Architect' : (dbSession.role_id || 'unknown');
|
|
409
|
+
// Create a PtySession backed by the reconnected shellper client
|
|
410
|
+
const session = manager.createSessionRaw({ label, cwd: workspacePath });
|
|
411
|
+
const ptySession = manager.getSession(session.id);
|
|
412
|
+
if (ptySession) {
|
|
413
|
+
ptySession.attachShellper(client, replayData, dbSession.shellper_pid, dbSession.id);
|
|
414
|
+
}
|
|
415
|
+
// Register in workspaceTerminals Map
|
|
416
|
+
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
417
|
+
if (dbSession.type === 'architect') {
|
|
418
|
+
entry.architect = session.id;
|
|
419
|
+
}
|
|
420
|
+
else if (dbSession.type === 'builder') {
|
|
421
|
+
entry.builders.set(dbSession.role_id || dbSession.id, session.id);
|
|
422
|
+
}
|
|
423
|
+
else if (dbSession.type === 'shell') {
|
|
424
|
+
entry.shells.set(dbSession.role_id || dbSession.id, session.id);
|
|
425
|
+
}
|
|
426
|
+
// Update SQLite with new terminal ID
|
|
427
|
+
db.prepare('DELETE FROM terminal_sessions WHERE id = ?').run(dbSession.id);
|
|
428
|
+
saveTerminalSession(session.id, workspacePath, dbSession.type, dbSession.role_id, dbSession.shellper_pid, dbSession.shellper_socket, dbSession.shellper_pid, dbSession.shellper_start_time);
|
|
429
|
+
_deps.registerKnownWorkspace(workspacePath);
|
|
430
|
+
// Clean up on exit
|
|
431
|
+
if (ptySession) {
|
|
432
|
+
ptySession.on('exit', () => {
|
|
433
|
+
const currentEntry = getWorkspaceTerminalsEntry(workspacePath);
|
|
434
|
+
if (dbSession.type === 'architect' && currentEntry.architect === session.id) {
|
|
435
|
+
currentEntry.architect = undefined;
|
|
436
|
+
}
|
|
437
|
+
deleteTerminalSession(session.id);
|
|
438
|
+
});
|
|
391
439
|
}
|
|
440
|
+
matchedSessionIds.add(dbSession.id);
|
|
441
|
+
shellperReconnected++;
|
|
442
|
+
_deps.log('INFO', `Reconnected shellper session → ${session.id} (${dbSession.type} for ${path.basename(workspacePath)})`);
|
|
392
443
|
}
|
|
393
444
|
// ---- Phase 2: Sweep stale SQLite rows ----
|
|
394
445
|
for (const session of allDbSessions) {
|
|
@@ -399,7 +450,7 @@ export async function reconcileTerminalSessions() {
|
|
|
399
450
|
continue;
|
|
400
451
|
// Stale row — kill orphaned process if any, then delete
|
|
401
452
|
if (session.pid && processExists(session.pid)) {
|
|
402
|
-
_deps.log('INFO', `Killing orphaned process: PID ${session.pid} (${session.type} for ${path.basename(session.
|
|
453
|
+
_deps.log('INFO', `Killing orphaned process: PID ${session.pid} (${session.type} for ${path.basename(session.workspace_path)})`);
|
|
403
454
|
try {
|
|
404
455
|
process.kill(session.pid, 'SIGTERM');
|
|
405
456
|
killed++;
|
|
@@ -418,74 +469,47 @@ export async function reconcileTerminalSessions() {
|
|
|
418
469
|
}
|
|
419
470
|
}
|
|
420
471
|
// ============================================================================
|
|
421
|
-
// Gate watcher
|
|
422
|
-
// ============================================================================
|
|
423
|
-
/** Start periodic gate status polling */
|
|
424
|
-
export function startGateWatcher() {
|
|
425
|
-
if (!_deps)
|
|
426
|
-
return;
|
|
427
|
-
gateWatcherInterval = setInterval(async () => {
|
|
428
|
-
if (!_deps)
|
|
429
|
-
return;
|
|
430
|
-
const projectPaths = _deps.getKnownProjectPaths();
|
|
431
|
-
for (const projectPath of projectPaths) {
|
|
432
|
-
try {
|
|
433
|
-
const gateStatus = getGateStatusForProject(projectPath);
|
|
434
|
-
await gateWatcher.checkAndNotify(gateStatus, projectPath);
|
|
435
|
-
}
|
|
436
|
-
catch (err) {
|
|
437
|
-
_deps.log('WARN', `Gate watcher error for ${projectPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}, 10_000);
|
|
441
|
-
}
|
|
442
|
-
/** Stop the gate watcher interval */
|
|
443
|
-
export function stopGateWatcher() {
|
|
444
|
-
if (gateWatcherInterval) {
|
|
445
|
-
clearInterval(gateWatcherInterval);
|
|
446
|
-
gateWatcherInterval = null;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
// ============================================================================
|
|
450
472
|
// Terminal list assembly
|
|
451
473
|
// ============================================================================
|
|
452
474
|
/**
|
|
453
|
-
* Get terminal list for a
|
|
475
|
+
* Get terminal list for a workspace from tower's registry.
|
|
454
476
|
* Phase 4 (Spec 0090): Tower manages terminals directly, no dashboard-server fetch.
|
|
455
477
|
* Returns architect, builders, and shells with their URLs.
|
|
456
478
|
*/
|
|
457
|
-
export async function
|
|
479
|
+
export async function getTerminalsForWorkspace(workspacePath, proxyUrl) {
|
|
458
480
|
const manager = getTerminalManager();
|
|
459
481
|
const terminals = [];
|
|
460
482
|
// Query SQLite first, then augment with shellper reconnection
|
|
461
|
-
const dbSessions =
|
|
483
|
+
const dbSessions = getTerminalSessionsForWorkspace(workspacePath);
|
|
462
484
|
// Use normalized path for cache consistency
|
|
463
|
-
const normalizedPath =
|
|
485
|
+
const normalizedPath = normalizeWorkspacePath(workspacePath);
|
|
464
486
|
// Build a fresh entry from SQLite, then replace atomically to avoid
|
|
465
487
|
// destroying in-memory state that was registered via POST /api/terminals.
|
|
466
488
|
// Previous approach cleared the cache then rebuilt, which lost terminals
|
|
467
489
|
// if their SQLite rows were deleted by external interference (e.g., tests).
|
|
468
490
|
const freshEntry = { builders: new Map(), shells: new Map(), fileTabs: new Map() };
|
|
469
491
|
// Load file tabs from SQLite (persisted across restarts)
|
|
470
|
-
const existingEntry =
|
|
492
|
+
const existingEntry = workspaceTerminals.get(normalizedPath);
|
|
471
493
|
if (existingEntry && existingEntry.fileTabs.size > 0) {
|
|
472
494
|
// Use in-memory state if already populated (avoids redundant DB reads)
|
|
473
495
|
freshEntry.fileTabs = existingEntry.fileTabs;
|
|
474
496
|
}
|
|
475
497
|
else {
|
|
476
|
-
freshEntry.fileTabs =
|
|
498
|
+
freshEntry.fileTabs = loadFileTabsForWorkspace(workspacePath);
|
|
477
499
|
}
|
|
478
500
|
for (const dbSession of dbSessions) {
|
|
479
501
|
// Verify session still exists in TerminalManager (runtime state)
|
|
480
502
|
let session = manager.getSession(dbSession.id);
|
|
481
|
-
if (!session && dbSession.shellper_socket && _deps?.shellperManager) {
|
|
503
|
+
if (!session && dbSession.shellper_socket && _deps?.shellperManager && !_reconciling) {
|
|
482
504
|
// PTY session gone but shellper may still be alive — reconnect on-the-fly
|
|
505
|
+
// Skip during reconciliation to avoid racing with reconcileTerminalSessions()
|
|
506
|
+
// which also reconnects to shellpers (Bugfix #274).
|
|
483
507
|
try {
|
|
484
508
|
// Restore auto-restart for architect sessions (same as startup reconciliation)
|
|
485
509
|
let restartOptions;
|
|
486
510
|
if (dbSession.type === 'architect') {
|
|
487
511
|
let architectCmd = 'claude';
|
|
488
|
-
const configPath = path.join(dbSession.
|
|
512
|
+
const configPath = path.join(dbSession.workspace_path, 'af-config.json');
|
|
489
513
|
if (fs.existsSync(configPath)) {
|
|
490
514
|
try {
|
|
491
515
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
@@ -500,39 +524,41 @@ export async function getTerminalsForProject(projectPath, proxyUrl) {
|
|
|
500
524
|
delete cleanEnv['CLAUDECODE'];
|
|
501
525
|
restartOptions = {
|
|
502
526
|
command: cmdParts[0],
|
|
503
|
-
args: buildArchitectArgs(cmdParts.slice(1), dbSession.
|
|
504
|
-
cwd: dbSession.
|
|
527
|
+
args: buildArchitectArgs(cmdParts.slice(1), dbSession.workspace_path),
|
|
528
|
+
cwd: dbSession.workspace_path,
|
|
505
529
|
env: cleanEnv,
|
|
506
530
|
restartDelay: 2000,
|
|
507
531
|
maxRestarts: 50,
|
|
508
532
|
};
|
|
509
533
|
}
|
|
534
|
+
_deps.log('INFO', `On-the-fly shellper reconnect for ${dbSession.id}`);
|
|
510
535
|
const client = await _deps.shellperManager.reconnectSession(dbSession.id, dbSession.shellper_socket, dbSession.shellper_pid, dbSession.shellper_start_time, restartOptions);
|
|
511
536
|
if (client) {
|
|
512
537
|
const replayData = client.getReplayData() ?? Buffer.alloc(0);
|
|
513
|
-
const label = dbSession.type === 'architect' ? 'Architect' :
|
|
514
|
-
const newSession = manager.createSessionRaw({ label, cwd: dbSession.
|
|
538
|
+
const label = dbSession.type === 'architect' ? 'Architect' : (dbSession.role_id || dbSession.id);
|
|
539
|
+
const newSession = manager.createSessionRaw({ label, cwd: dbSession.workspace_path });
|
|
515
540
|
const ptySession = manager.getSession(newSession.id);
|
|
516
541
|
if (ptySession) {
|
|
517
542
|
ptySession.attachShellper(client, replayData, dbSession.shellper_pid, dbSession.id);
|
|
518
543
|
// Clean up on exit (same as startup reconciliation path)
|
|
519
544
|
ptySession.on('exit', () => {
|
|
520
|
-
const currentEntry =
|
|
545
|
+
const currentEntry = getWorkspaceTerminalsEntry(dbSession.workspace_path);
|
|
521
546
|
if (dbSession.type === 'architect' && currentEntry.architect === newSession.id) {
|
|
522
547
|
currentEntry.architect = undefined;
|
|
523
548
|
}
|
|
524
549
|
deleteTerminalSession(newSession.id);
|
|
525
550
|
});
|
|
526
551
|
}
|
|
552
|
+
const originalSessionId = dbSession.id;
|
|
527
553
|
deleteTerminalSession(dbSession.id);
|
|
528
|
-
saveTerminalSession(newSession.id, dbSession.
|
|
554
|
+
saveTerminalSession(newSession.id, dbSession.workspace_path, dbSession.type, dbSession.role_id, dbSession.shellper_pid, dbSession.shellper_socket, dbSession.shellper_pid, dbSession.shellper_start_time);
|
|
529
555
|
dbSession.id = newSession.id;
|
|
530
556
|
session = manager.getSession(newSession.id);
|
|
531
|
-
_deps.log('INFO', `
|
|
557
|
+
_deps.log('INFO', `On-the-fly reconnect succeeded for ${originalSessionId} → ${newSession.id}`);
|
|
532
558
|
}
|
|
533
559
|
}
|
|
534
560
|
catch (err) {
|
|
535
|
-
_deps.log('WARN', `
|
|
561
|
+
_deps.log('WARN', `On-the-fly reconnect failed for ${dbSession.id}: ${err.message}`);
|
|
536
562
|
}
|
|
537
563
|
}
|
|
538
564
|
if (!session) {
|
|
@@ -556,8 +582,8 @@ export async function getTerminalsForProject(projectPath, proxyUrl) {
|
|
|
556
582
|
terminals.push({
|
|
557
583
|
type: 'builder',
|
|
558
584
|
id: builderId,
|
|
559
|
-
label:
|
|
560
|
-
url: `${proxyUrl}?tab
|
|
585
|
+
label: builderId,
|
|
586
|
+
url: `${proxyUrl}?tab=${builderId}`,
|
|
561
587
|
active: true,
|
|
562
588
|
});
|
|
563
589
|
}
|
|
@@ -568,7 +594,7 @@ export async function getTerminalsForProject(projectPath, proxyUrl) {
|
|
|
568
594
|
type: 'shell',
|
|
569
595
|
id: shellId,
|
|
570
596
|
label: `Shell ${shellId.replace('shell-', '')}`,
|
|
571
|
-
url: `${proxyUrl}?tab
|
|
597
|
+
url: `${proxyUrl}?tab=${shellId}`,
|
|
572
598
|
active: true,
|
|
573
599
|
});
|
|
574
600
|
}
|
|
@@ -597,8 +623,8 @@ export async function getTerminalsForProject(projectPath, proxyUrl) {
|
|
|
597
623
|
terminals.push({
|
|
598
624
|
type: 'builder',
|
|
599
625
|
id: builderId,
|
|
600
|
-
label:
|
|
601
|
-
url: `${proxyUrl}?tab
|
|
626
|
+
label: builderId,
|
|
627
|
+
url: `${proxyUrl}?tab=${builderId}`,
|
|
602
628
|
active: true,
|
|
603
629
|
});
|
|
604
630
|
}
|
|
@@ -613,7 +639,7 @@ export async function getTerminalsForProject(projectPath, proxyUrl) {
|
|
|
613
639
|
type: 'shell',
|
|
614
640
|
id: shellId,
|
|
615
641
|
label: `Shell ${shellId.replace('shell-', '')}`,
|
|
616
|
-
url: `${proxyUrl}?tab
|
|
642
|
+
url: `${proxyUrl}?tab=${shellId}`,
|
|
617
643
|
active: true,
|
|
618
644
|
});
|
|
619
645
|
}
|
|
@@ -621,9 +647,7 @@ export async function getTerminalsForProject(projectPath, proxyUrl) {
|
|
|
621
647
|
}
|
|
622
648
|
}
|
|
623
649
|
// Atomically replace the cache entry
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const gateStatus = getGateStatusForProject(projectPath);
|
|
627
|
-
return { terminals, gateStatus };
|
|
650
|
+
workspaceTerminals.set(normalizedPath, freshEntry);
|
|
651
|
+
return { terminals };
|
|
628
652
|
}
|
|
629
653
|
//# sourceMappingURL=tower-terminals.js.map
|