@phnx-labs/agents-cli 1.22.120 → 1.22.121
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/CHANGELOG.md +227 -0
- package/README.md +19 -2
- package/dist/bootstrap.d.ts +0 -15
- package/dist/bootstrap.js +10 -409
- package/dist/cli/command-registry.d.ts +0 -58
- package/dist/cli/command-registry.js +2 -74
- package/dist/commands/accounts.d.ts +0 -17
- package/dist/commands/accounts.js +1 -52
- package/dist/commands/alias.d.ts +0 -12
- package/dist/commands/alias.js +0 -6
- package/dist/commands/apply.d.ts +0 -15
- package/dist/commands/apply.js +0 -63
- package/dist/commands/audit.d.ts +0 -12
- package/dist/commands/audit.js +0 -13
- package/dist/commands/auth.js +4 -12
- package/dist/commands/beta.d.ts +0 -6
- package/dist/commands/beta.js +2 -6
- package/dist/commands/browser-sessions-picker.d.ts +0 -8
- package/dist/commands/browser-sessions-picker.js +0 -26
- package/dist/commands/browser.d.ts +0 -57
- package/dist/commands/browser.js +0 -80
- package/dist/commands/budget.d.ts +0 -13
- package/dist/commands/budget.js +0 -2
- package/dist/commands/cli.d.ts +0 -12
- package/dist/commands/cli.js +0 -4
- package/dist/commands/cloud.d.ts +0 -9
- package/dist/commands/cloud.js +0 -43
- package/dist/commands/commands.d.ts +0 -9
- package/dist/commands/commands.js +0 -18
- package/dist/commands/computer-sessions-picker.d.ts +0 -11
- package/dist/commands/computer-sessions-picker.js +0 -29
- package/dist/commands/computer.d.ts +0 -62
- package/dist/commands/computer.js +3 -114
- package/dist/commands/config.d.ts +0 -14
- package/dist/commands/config.js +21 -60
- package/dist/commands/cost.d.ts +0 -11
- package/dist/commands/cost.js +1 -17
- package/dist/commands/daemon-test-harness.d.ts +0 -32
- package/dist/commands/daemon-test-harness.js +1 -36
- package/dist/commands/daemon.d.ts +0 -46
- package/dist/commands/daemon.js +8 -255
- package/dist/commands/detach-core.d.ts +0 -28
- package/dist/commands/detach-core.js +0 -30
- package/dist/commands/detach.d.ts +1 -12
- package/dist/commands/detach.js +16 -52
- package/dist/commands/device-config-test-harness.js +0 -12
- package/dist/commands/doctor.d.ts +0 -103
- package/dist/commands/doctor.js +3 -279
- package/dist/commands/events.d.ts +0 -29
- package/dist/commands/events.js +1 -55
- package/dist/commands/exec.d.ts +0 -163
- package/dist/commands/exec.js +15 -964
- package/dist/commands/factory.d.ts +0 -6
- package/dist/commands/factory.js +0 -2
- package/dist/commands/feed-watch.js +0 -20
- package/dist/commands/feed.d.ts +0 -38
- package/dist/commands/feed.js +1 -151
- package/dist/commands/feedback.d.ts +0 -5
- package/dist/commands/feedback.js +0 -5
- package/dist/commands/fleet-capture.d.ts +0 -12
- package/dist/commands/fleet-capture.js +1 -39
- package/dist/commands/focus.d.ts +6 -94
- package/dist/commands/focus.js +13 -183
- package/dist/commands/fork.d.ts +0 -21
- package/dist/commands/fork.js +0 -45
- package/dist/commands/funnel.d.ts +0 -3
- package/dist/commands/go.d.ts +2 -71
- package/dist/commands/go.js +5 -99
- package/dist/commands/harness-hooks.d.ts +0 -41
- package/dist/commands/harness-hooks.js +0 -48
- package/dist/commands/harness-wizard.d.ts +0 -137
- package/dist/commands/harness-wizard.js +0 -109
- package/dist/commands/harness.d.ts +0 -63
- package/dist/commands/harness.js +0 -103
- package/dist/commands/hooks.d.ts +0 -9
- package/dist/commands/hooks.js +0 -26
- package/dist/commands/humans.d.ts +0 -8
- package/dist/commands/humans.js +0 -8
- package/dist/commands/import.d.ts +0 -22
- package/dist/commands/import.js +0 -79
- package/dist/commands/insights.d.ts +0 -37
- package/dist/commands/insights.js +3 -115
- package/dist/commands/inspect.d.ts +0 -117
- package/dist/commands/inspect.js +2 -448
- package/dist/commands/lease.d.ts +0 -28
- package/dist/commands/lease.js +0 -43
- package/dist/commands/logs.d.ts +0 -23
- package/dist/commands/logs.js +0 -30
- package/dist/commands/mailboxes.d.ts +0 -18
- package/dist/commands/mailboxes.js +0 -29
- package/dist/commands/mcp.d.ts +0 -9
- package/dist/commands/mcp.js +0 -53
- package/dist/commands/memory.d.ts +0 -7
- package/dist/commands/memory.js +0 -7
- package/dist/commands/menubar.d.ts +0 -8
- package/dist/commands/menubar.js +0 -24
- package/dist/commands/message.d.ts +0 -23
- package/dist/commands/message.js +0 -50
- package/dist/commands/mine.d.ts +0 -22
- package/dist/commands/mine.js +0 -21
- package/dist/commands/models.d.ts +0 -8
- package/dist/commands/models.js +0 -33
- package/dist/commands/modes.d.ts +0 -10
- package/dist/commands/modes.js +0 -13
- package/dist/commands/monitors.d.ts +0 -9
- package/dist/commands/monitors.js +3 -160
- package/dist/commands/open.d.ts +0 -26
- package/dist/commands/open.js +0 -10
- package/dist/commands/output.d.ts +0 -18
- package/dist/commands/output.js +1 -34
- package/dist/commands/packages-materialize.d.ts +0 -11
- package/dist/commands/packages-materialize.js +0 -3
- package/dist/commands/packages.d.ts +0 -10
- package/dist/commands/packages.js +0 -50
- package/dist/commands/perf.d.ts +0 -41
- package/dist/commands/perf.js +0 -55
- package/dist/commands/permissions.d.ts +0 -9
- package/dist/commands/permissions.js +0 -43
- package/dist/commands/plugins.d.ts +0 -14
- package/dist/commands/plugins.js +2 -55
- package/dist/commands/profiles.d.ts +0 -25
- package/dist/commands/profiles.js +0 -27
- package/dist/commands/projects-todo.d.ts +2 -0
- package/dist/commands/projects-todo.js +132 -0
- package/dist/commands/projects.d.ts +0 -85
- package/dist/commands/projects.js +344 -291
- package/dist/commands/prune.js +0 -30
- package/dist/commands/ps-roster.d.ts +92 -0
- package/dist/commands/ps-roster.js +744 -0
- package/dist/commands/ps.d.ts +6 -0
- package/dist/commands/ps.js +98 -0
- package/dist/commands/refresh-rules.d.ts +0 -13
- package/dist/commands/refresh-rules.js +0 -14
- package/dist/commands/reminders.d.ts +0 -7
- package/dist/commands/repo.d.ts +0 -89
- package/dist/commands/repo.js +7 -205
- package/dist/commands/resource-view.d.ts +0 -33
- package/dist/commands/resource-view.js +0 -49
- package/dist/commands/resume.d.ts +1 -19
- package/dist/commands/resume.js +0 -50
- package/dist/commands/route.d.ts +0 -10
- package/dist/commands/route.js +0 -19
- package/dist/commands/routines.d.ts +0 -42
- package/dist/commands/routines.js +5 -290
- package/dist/commands/routines.test-fixture.d.ts +0 -34
- package/dist/commands/routines.test-fixture.js +4 -100
- package/dist/commands/rules.d.ts +0 -9
- package/dist/commands/rules.js +1 -6
- package/dist/commands/run-account-picker.d.ts +0 -59
- package/dist/commands/run-account-picker.js +0 -71
- package/dist/commands/run-broadcast.d.ts +0 -9
- package/dist/commands/run-broadcast.js +0 -7
- package/dist/commands/run-cloud.d.ts +0 -15
- package/dist/commands/run-cloud.js +0 -37
- package/dist/commands/run-device-picker.d.ts +0 -25
- package/dist/commands/run-device-picker.js +1 -53
- package/dist/commands/secrets-passthrough.d.ts +0 -31
- package/dist/commands/secrets-passthrough.js +0 -10
- package/dist/commands/send.d.ts +0 -20
- package/dist/commands/send.js +14 -18
- package/dist/commands/sessions-backfill.d.ts +0 -7
- package/dist/commands/sessions-backfill.js +1 -16
- package/dist/commands/sessions-backup-setup.js +0 -11
- package/dist/commands/sessions-bookmark.d.ts +0 -8
- package/dist/commands/sessions-bookmark.js +0 -28
- package/dist/commands/sessions-browser.d.ts +2 -117
- package/dist/commands/sessions-browser.js +5 -243
- package/dist/commands/sessions-export.d.ts +1 -26
- package/dist/commands/sessions-export.js +3 -81
- package/dist/commands/sessions-import.d.ts +0 -14
- package/dist/commands/sessions-import.js +0 -59
- package/dist/commands/sessions-inject.d.ts +0 -60
- package/dist/commands/sessions-inject.js +3 -96
- package/dist/commands/sessions-migrate.d.ts +2 -37
- package/dist/commands/sessions-migrate.js +7 -159
- package/dist/commands/sessions-optimize.d.ts +0 -12
- package/dist/commands/sessions-picker-factory.d.ts +0 -7
- package/dist/commands/sessions-picker-factory.js +0 -8
- package/dist/commands/sessions-picker.d.ts +4 -103
- package/dist/commands/sessions-picker.js +13 -352
- package/dist/commands/sessions-render.d.ts +1 -2
- package/dist/commands/sessions-render.js +3 -7
- package/dist/commands/sessions-resume.d.ts +1 -24
- package/dist/commands/sessions-resume.js +2 -65
- package/dist/commands/sessions-share.d.ts +1 -9
- package/dist/commands/sessions-share.js +1 -45
- package/dist/commands/sessions-stats.d.ts +0 -24
- package/dist/commands/sessions-stats.js +0 -49
- package/dist/commands/sessions-stop.d.ts +1 -20
- package/dist/commands/sessions-stop.js +4 -9
- package/dist/commands/sessions-tail.d.ts +1 -19
- package/dist/commands/sessions-tail.js +3 -36
- package/dist/commands/sessions-trace.d.ts +5 -36
- package/dist/commands/sessions-trace.js +7 -82
- package/dist/commands/sessions.d.ts +1 -666
- package/dist/commands/sessions.js +30 -2279
- package/dist/commands/sessions.test-fixture.d.ts +0 -9
- package/dist/commands/sessions.test-fixture.js +8 -23
- package/dist/commands/setup-accounts.d.ts +0 -6
- package/dist/commands/setup-browser.d.ts +0 -23
- package/dist/commands/setup-browser.js +0 -28
- package/dist/commands/setup-computer.d.ts +0 -16
- package/dist/commands/setup-computer.js +0 -28
- package/dist/commands/setup-fleet.d.ts +0 -8
- package/dist/commands/setup-fleet.js +0 -11
- package/dist/commands/setup-mine.d.ts +0 -14
- package/dist/commands/setup-mine.js +0 -25
- package/dist/commands/setup-preferences.d.ts +0 -47
- package/dist/commands/setup-preferences.js +0 -53
- package/dist/commands/setup-secrets.d.ts +2 -24
- package/dist/commands/setup-secrets.js +0 -24
- package/dist/commands/setup-term.d.ts +0 -21
- package/dist/commands/setup-term.js +0 -21
- package/dist/commands/setup-terminal.d.ts +0 -1
- package/dist/commands/setup-terminal.js +0 -1
- package/dist/commands/setup-tools.d.ts +2 -0
- package/dist/commands/setup-tools.js +73 -0
- package/dist/commands/setup.d.ts +0 -20
- package/dist/commands/setup.js +4 -57
- package/dist/commands/skills.d.ts +0 -9
- package/dist/commands/skills.js +1 -22
- package/dist/commands/snapshot.d.ts +0 -9
- package/dist/commands/snapshot.js +0 -10
- package/dist/commands/ssh.d.ts +0 -64
- package/dist/commands/ssh.js +9 -420
- package/dist/commands/status.d.ts +0 -12
- package/dist/commands/status.js +0 -26
- package/dist/commands/subagents.d.ts +0 -8
- package/dist/commands/subagents.js +0 -31
- package/dist/commands/sync.d.ts +0 -37
- package/dist/commands/sync.js +0 -256
- package/dist/commands/teams-picker.d.ts +0 -3
- package/dist/commands/teams-picker.js +0 -34
- package/dist/commands/teams.d.ts +1 -76
- package/dist/commands/teams.js +9 -477
- package/dist/commands/tmux.d.ts +0 -25
- package/dist/commands/tmux.js +0 -46
- package/dist/commands/traces.js +0 -19
- package/dist/commands/trash.d.ts +0 -17
- package/dist/commands/trash.js +1 -11
- package/dist/commands/uninstall.d.ts +0 -9
- package/dist/commands/uninstall.js +0 -6
- package/dist/commands/update.d.ts +0 -22
- package/dist/commands/update.js +0 -44
- package/dist/commands/upgrade.d.ts +0 -1
- package/dist/commands/upgrade.js +0 -1
- package/dist/commands/utils.d.ts +0 -85
- package/dist/commands/utils.js +1 -95
- package/dist/commands/versions.d.ts +0 -16
- package/dist/commands/versions.js +0 -134
- package/dist/commands/view.d.ts +0 -90
- package/dist/commands/view.js +4 -343
- package/dist/commands/watchdog.d.ts +0 -19
- package/dist/commands/watchdog.js +3 -43
- package/dist/commands/webhook.d.ts +0 -8
- package/dist/commands/webhook.js +0 -14
- package/dist/commands/workflows.d.ts +0 -8
- package/dist/commands/workflows.js +0 -6
- package/dist/index.d.ts +0 -25
- package/dist/index.js +5 -119
- package/dist/lib/account-capabilities.d.ts +0 -34
- package/dist/lib/account-capabilities.js +0 -32
- package/dist/lib/account-catalog.d.ts +0 -128
- package/dist/lib/account-catalog.js +8 -154
- package/dist/lib/account-provider-registry.d.ts +0 -14
- package/dist/lib/account-provider-registry.js +0 -8
- package/dist/lib/account-registry.d.ts +0 -74
- package/dist/lib/account-registry.js +14 -189
- package/dist/lib/account-schema.d.ts +0 -19
- package/dist/lib/account-schema.js +0 -43
- package/dist/lib/accounting/account-launch.d.ts +0 -15
- package/dist/lib/accounting/account-launch.js +1 -3
- package/dist/lib/accounting/account-pool-collect.d.ts +0 -26
- package/dist/lib/accounting/account-pool-collect.js +0 -42
- package/dist/lib/accounting/account-pool.d.ts +0 -41
- package/dist/lib/accounting/account-pool.js +0 -17
- package/dist/lib/accounting/capacity.d.ts +0 -32
- package/dist/lib/accounting/capacity.js +0 -32
- package/dist/lib/accounting/rotate.d.ts +0 -518
- package/dist/lib/accounting/rotate.js +4 -598
- package/dist/lib/accounting/usage-ingest.d.ts +0 -1
- package/dist/lib/accounting/usage-ingest.js +0 -44
- package/dist/lib/accounting/usage-sync.d.ts +0 -60
- package/dist/lib/accounting/usage-sync.js +4 -51
- package/dist/lib/accounting/usage.d.ts +0 -632
- package/dist/lib/accounting/usage.js +14 -1055
- package/dist/lib/accounts/add.d.ts +0 -60
- package/dist/lib/accounts/add.js +0 -127
- package/dist/lib/accounts/auth-operation-lock.d.ts +0 -1
- package/dist/lib/accounts/auth-operation-lock.js +0 -4
- package/dist/lib/accounts/migrate.d.ts +0 -9
- package/dist/lib/accounts/migrate.js +3 -42
- package/dist/lib/accounts/slots.d.ts +0 -25
- package/dist/lib/accounts/slots.js +1 -49
- package/dist/lib/acp/client.d.ts +0 -16
- package/dist/lib/acp/client.js +0 -29
- package/dist/lib/acp/harnesses.d.ts +0 -15
- package/dist/lib/acp/harnesses.js +0 -16
- package/dist/lib/acp/run.d.ts +0 -8
- package/dist/lib/acp/run.js +0 -8
- package/dist/lib/actor.d.ts +1 -71
- package/dist/lib/actor.js +1 -99
- package/dist/lib/add-dir.d.ts +0 -46
- package/dist/lib/add-dir.js +1 -70
- package/dist/lib/address.d.ts +0 -17
- package/dist/lib/address.js +0 -15
- package/dist/lib/agent-cli-commands.d.ts +0 -10
- package/dist/lib/agent-cli-commands.js +0 -10
- package/dist/lib/agent-modes.d.ts +0 -29
- package/dist/lib/agent-modes.js +0 -31
- package/dist/lib/agent-spec/agents.d.ts +0 -426
- package/dist/lib/agent-spec/agents.js +37 -924
- package/dist/lib/agent-spec/index.d.ts +0 -5
- package/dist/lib/agent-spec/index.js +0 -14
- package/dist/lib/agent-spec/materialize.d.ts +0 -10
- package/dist/lib/agent-spec/materialize.js +2 -139
- package/dist/lib/agent-spec/package-resolve.d.ts +0 -8
- package/dist/lib/agent-spec/package-resolve.js +1 -44
- package/dist/lib/agent-spec/package-schema.d.ts +0 -2
- package/dist/lib/agent-spec/package-schema.js +0 -11
- package/dist/lib/agent-spec/package-types.d.ts +0 -27
- package/dist/lib/agent-spec/package-types.js +0 -1
- package/dist/lib/agent-spec/primitives.d.ts +0 -18
- package/dist/lib/agent-spec/primitives.js +0 -27
- package/dist/lib/agent-spec/provider.js +0 -2
- package/dist/lib/agent-spec/resolve.d.ts +0 -23
- package/dist/lib/agent-spec/resolve.js +0 -41
- package/dist/lib/agent-spec/types.d.ts +0 -34
- package/dist/lib/agent-spec/types.js +0 -6
- package/dist/lib/agents.d.ts +0 -1
- package/dist/lib/agents.js +0 -1
- package/dist/lib/analytics/mix-commands.d.ts +0 -23
- package/dist/lib/analytics/mix-commands.js +0 -26
- package/dist/lib/analytics/recipes.d.ts +0 -1
- package/dist/lib/analytics/recipes.js +5 -10
- package/dist/lib/analytics/usage-db.js +4 -6
- package/dist/lib/answer-router.d.ts +0 -61
- package/dist/lib/answer-router.js +0 -57
- package/dist/lib/app-bundle-install.d.ts +0 -13
- package/dist/lib/app-bundle-install.js +0 -50
- package/dist/lib/artifact-actions.d.ts +0 -15
- package/dist/lib/artifact-actions.js +0 -18
- package/dist/lib/artifacts-client.d.ts +0 -10
- package/dist/lib/artifacts-client.js +0 -17
- package/dist/lib/ask-classifier.d.ts +0 -38
- package/dist/lib/ask-classifier.js +0 -27
- package/dist/lib/audit/log.d.ts +0 -47
- package/dist/lib/audit/log.js +1 -59
- package/dist/lib/auth-health.d.ts +0 -213
- package/dist/lib/auth-health.js +16 -266
- package/dist/lib/auth-mint.d.ts +0 -83
- package/dist/lib/auth-mint.js +8 -85
- package/dist/lib/auto-pull-worker.d.ts +0 -10
- package/dist/lib/auto-pull-worker.js +3 -48
- package/dist/lib/auto-pull.d.ts +0 -64
- package/dist/lib/auto-pull.js +0 -79
- package/dist/lib/beta.d.ts +0 -9
- package/dist/lib/beta.js +0 -9
- package/dist/lib/binary-shadow.d.ts +0 -22
- package/dist/lib/binary-shadow.js +4 -34
- package/dist/lib/boot-profile.d.ts +0 -10
- package/dist/lib/boot-profile.js +1 -31
- package/dist/lib/brand.d.ts +0 -20
- package/dist/lib/brand.js +0 -50
- package/dist/lib/browser/context.d.ts +0 -54
- package/dist/lib/browser/context.js +0 -49
- package/dist/lib/browser/paths.d.ts +0 -21
- package/dist/lib/browser/paths.js +0 -26
- package/dist/lib/browser/record.d.ts +0 -34
- package/dist/lib/browser/record.js +0 -38
- package/dist/lib/browser/sessions-list.d.ts +1 -79
- package/dist/lib/browser/sessions-list.js +2 -98
- package/dist/lib/browser-client.d.ts +0 -112
- package/dist/lib/browser-client.js +1 -90
- package/dist/lib/budget/config.d.ts +0 -6
- package/dist/lib/budget/config.js +0 -29
- package/dist/lib/budget/enforce.d.ts +0 -50
- package/dist/lib/budget/enforce.js +0 -27
- package/dist/lib/budget/ledger.d.ts +0 -23
- package/dist/lib/budget/ledger.js +0 -28
- package/dist/lib/budget/live-cloud.d.ts +0 -26
- package/dist/lib/budget/live-cloud.js +0 -12
- package/dist/lib/budget/live-team.d.ts +0 -17
- package/dist/lib/budget/live-team.js +1 -35
- package/dist/lib/budget/preflight.d.ts +0 -51
- package/dist/lib/budget/preflight.js +0 -36
- package/dist/lib/byok-usage.d.ts +0 -20
- package/dist/lib/byok-usage.js +1 -24
- package/dist/lib/capabilities.d.ts +0 -24
- package/dist/lib/capabilities.js +0 -29
- package/dist/lib/catchup.d.ts +0 -87
- package/dist/lib/catchup.js +1 -81
- package/dist/lib/channels/owner-forward.d.ts +0 -70
- package/dist/lib/channels/owner-forward.js +1 -43
- package/dist/lib/channels/owner-sink.js +0 -21
- package/dist/lib/channels/providers/desktop.d.ts +0 -37
- package/dist/lib/channels/providers/desktop.js +0 -72
- package/dist/lib/channels/providers/index.d.ts +0 -1
- package/dist/lib/channels/providers/index.js +2 -6
- package/dist/lib/channels/providers/mailbox.js +0 -7
- package/dist/lib/channels/providers/openclaw-telegram.js +0 -8
- package/dist/lib/channels/providers/rush.d.ts +0 -2
- package/dist/lib/channels/providers/rush.js +0 -26
- package/dist/lib/channels/providers/session.d.ts +2 -0
- package/dist/lib/channels/providers/session.js +26 -0
- package/dist/lib/channels/registry.d.ts +0 -27
- package/dist/lib/channels/registry.js +0 -10
- package/dist/lib/channels/resolve.d.ts +0 -19
- package/dist/lib/channels/resolve.js +0 -2
- package/dist/lib/channels/send.d.ts +0 -50
- package/dist/lib/channels/send.js +0 -25
- package/dist/lib/checkpoint.d.ts +0 -36
- package/dist/lib/checkpoint.js +0 -28
- package/dist/lib/claude-account-token.d.ts +0 -98
- package/dist/lib/claude-account-token.js +7 -169
- package/dist/lib/claude-statusline.d.ts +0 -41
- package/dist/lib/claude-statusline.js +0 -68
- package/dist/lib/cli-entry.d.ts +0 -26
- package/dist/lib/cli-entry.js +1 -61
- package/dist/lib/cli-resources.d.ts +31 -89
- package/dist/lib/cli-resources.js +116 -152
- package/dist/lib/cloud/antigravity.d.ts +0 -29
- package/dist/lib/cloud/antigravity.js +0 -36
- package/dist/lib/cloud/codex.d.ts +0 -8
- package/dist/lib/cloud/codex.js +0 -41
- package/dist/lib/cloud/cursor.d.ts +0 -5
- package/dist/lib/cloud/cursor.js +2 -7
- package/dist/lib/cloud/dispatch.d.ts +0 -13
- package/dist/lib/cloud/dispatch.js +1 -48
- package/dist/lib/cloud/factory.d.ts +0 -52
- package/dist/lib/cloud/factory.js +3 -67
- package/dist/lib/cloud/host.d.ts +0 -39
- package/dist/lib/cloud/host.js +5 -48
- package/dist/lib/cloud/registry.d.ts +0 -25
- package/dist/lib/cloud/registry.js +0 -29
- package/dist/lib/cloud/rush.d.ts +0 -47
- package/dist/lib/cloud/rush.js +1 -82
- package/dist/lib/cloud/session-index.d.ts +0 -27
- package/dist/lib/cloud/session-index.js +1 -32
- package/dist/lib/cloud/store.d.ts +0 -18
- package/dist/lib/cloud/store.js +0 -28
- package/dist/lib/cloud/stream.d.ts +0 -14
- package/dist/lib/cloud/stream.js +0 -25
- package/dist/lib/cloud/types.d.ts +0 -168
- package/dist/lib/cloud/types.js +0 -46
- package/dist/lib/cloudflare/creds.d.ts +0 -2
- package/dist/lib/cloudflare/creds.js +0 -14
- package/dist/lib/cloudflare/provision.d.ts +0 -8
- package/dist/lib/cloudflare/provision.js +1 -19
- package/dist/lib/codex-home.d.ts +0 -60
- package/dist/lib/codex-home.js +2 -72
- package/dist/lib/codex-policy.d.ts +0 -10
- package/dist/lib/codex-policy.js +0 -13
- package/dist/lib/command-skills.d.ts +0 -10
- package/dist/lib/command-skills.js +0 -10
- package/dist/lib/commands.d.ts +0 -74
- package/dist/lib/commands.js +0 -112
- package/dist/lib/comms-render.d.ts +0 -5
- package/dist/lib/comms-render.js +0 -5
- package/dist/lib/computer/context.d.ts +0 -23
- package/dist/lib/computer/context.js +0 -14
- package/dist/lib/computer/policy.d.ts +0 -20
- package/dist/lib/computer/policy.js +0 -53
- package/dist/lib/computer/record.d.ts +0 -33
- package/dist/lib/computer/record.js +1 -48
- package/dist/lib/computer/sessions-list.d.ts +1 -128
- package/dist/lib/computer/sessions-list.js +6 -165
- package/dist/lib/computer-client.d.ts +0 -106
- package/dist/lib/computer-client.js +5 -86
- package/dist/lib/concurrency.d.ts +0 -14
- package/dist/lib/concurrency.js +0 -3
- package/dist/lib/config-drift.d.ts +0 -31
- package/dist/lib/config-drift.js +0 -32
- package/dist/lib/config-keys.d.ts +1 -65
- package/dist/lib/config-keys.js +6 -52
- package/dist/lib/config-transfer.d.ts +0 -14
- package/dist/lib/config-transfer.js +0 -26
- package/dist/lib/convert.d.ts +0 -17
- package/dist/lib/convert.js +0 -18
- package/dist/lib/crabbox/cli.d.ts +0 -156
- package/dist/lib/crabbox/cli.js +1 -188
- package/dist/lib/crabbox/config.d.ts +0 -18
- package/dist/lib/crabbox/config.js +1 -19
- package/dist/lib/crabbox/lease.d.ts +0 -93
- package/dist/lib/crabbox/lease.js +4 -148
- package/dist/lib/crabbox/progress.d.ts +0 -56
- package/dist/lib/crabbox/progress.js +2 -49
- package/dist/lib/crabbox/runtimes.d.ts +0 -94
- package/dist/lib/crabbox/runtimes.js +1 -111
- package/dist/lib/crabbox/setup-copy.d.ts +0 -64
- package/dist/lib/crabbox/setup-copy.js +1 -53
- package/dist/lib/daemon/account-state-daemon-service.d.ts +0 -48
- package/dist/lib/daemon/account-state-daemon-service.js +2 -63
- package/dist/lib/daemon/attention-notify-service.d.ts +0 -12
- package/dist/lib/daemon/attention-notify-service.js +1 -24
- package/dist/lib/daemon/auth-sync-service.d.ts +0 -28
- package/dist/lib/daemon/auth-sync-service.js +0 -56
- package/dist/lib/daemon/catchup-service.d.ts +0 -34
- package/dist/lib/daemon/catchup-service.js +0 -30
- package/dist/lib/daemon/daemon.d.ts +0 -374
- package/dist/lib/daemon/daemon.js +44 -1049
- package/dist/lib/daemon/daemon.test-fixture.d.ts +0 -19
- package/dist/lib/daemon/daemon.test-fixture.js +1 -24
- package/dist/lib/daemon/device-probe-service.d.ts +0 -16
- package/dist/lib/daemon/device-probe-service.js +0 -20
- package/dist/lib/daemon/feed-stream-service.d.ts +0 -15
- package/dist/lib/daemon/feed-stream-service.js +0 -20
- package/dist/lib/daemon/harness-update-service.d.ts +2 -61
- package/dist/lib/daemon/harness-update-service.js +6 -86
- package/dist/lib/daemon/heartbeat-service.d.ts +0 -1
- package/dist/lib/daemon/heartbeat-service.js +0 -1
- package/dist/lib/daemon/leaked-daemons.d.ts +0 -46
- package/dist/lib/daemon/leaked-daemons.js +2 -78
- package/dist/lib/daemon/monitor-engine-service.d.ts +0 -13
- package/dist/lib/daemon/monitor-engine-service.js +3 -15
- package/dist/lib/daemon/runner.d.ts +0 -194
- package/dist/lib/daemon/runner.js +39 -760
- package/dist/lib/daemon/self-heal-service.d.ts +18 -17
- package/dist/lib/daemon/self-heal-service.js +83 -37
- package/dist/lib/daemon/self-update-service.d.ts +1 -131
- package/dist/lib/daemon/self-update-service.js +26 -189
- package/dist/lib/daemon/service.d.ts +0 -73
- package/dist/lib/daemon/service.js +0 -34
- package/dist/lib/daemon/session-index-service.d.ts +0 -12
- package/dist/lib/daemon/session-index-service.js +0 -23
- package/dist/lib/daemon/session-state-service.d.ts +0 -8
- package/dist/lib/daemon/session-state-service.js +0 -8
- package/dist/lib/daemon/session-summarizer-service.d.ts +0 -14
- package/dist/lib/daemon/session-summarizer-service.js +1 -19
- package/dist/lib/daemon/session-title-service.d.ts +0 -25
- package/dist/lib/daemon/session-title-service.js +0 -32
- package/dist/lib/daemon/state-dir-check-service.d.ts +0 -24
- package/dist/lib/daemon/state-dir-check-service.js +0 -29
- package/dist/lib/daemon/supervisor.d.ts +0 -112
- package/dist/lib/daemon/supervisor.js +4 -147
- package/dist/lib/daemon/testdata/process-view-daemon.js +0 -2
- package/dist/lib/daemon/testdata/process-view-start.js +0 -5
- package/dist/lib/daemon/tmux-reap-service.d.ts +0 -1
- package/dist/lib/daemon/tmux-reap-service.js +0 -1
- package/dist/lib/daemon/usage-sync-service.d.ts +0 -23
- package/dist/lib/daemon/usage-sync-service.js +0 -26
- package/dist/lib/daemon/watchdog-service.d.ts +0 -9
- package/dist/lib/daemon/watchdog-service.js +0 -13
- package/dist/lib/daemon/webhook-receiver-service.d.ts +0 -1
- package/dist/lib/daemon/webhook-receiver-service.js +0 -1
- package/dist/lib/daemon-health.d.ts +0 -55
- package/dist/lib/daemon-health.js +4 -77
- package/dist/lib/daemon-services.d.ts +0 -36
- package/dist/lib/daemon-services.js +0 -45
- package/dist/lib/daemon-ticks.d.ts +0 -126
- package/dist/lib/daemon-ticks.js +1 -156
- package/dist/lib/daemon-webhooks.d.ts +0 -57
- package/dist/lib/daemon-webhooks.js +2 -54
- package/dist/lib/deeplink/register.d.ts +0 -15
- package/dist/lib/deeplink/register.js +0 -60
- package/dist/lib/deeplink/url.d.ts +0 -24
- package/dist/lib/deeplink/url.js +1 -33
- package/dist/lib/device-config.d.ts +1 -192
- package/dist/lib/device-config.js +86 -240
- package/dist/lib/devices/config-migration.d.ts +0 -43
- package/dist/lib/devices/config-migration.js +7 -127
- package/dist/lib/devices/connect.d.ts +0 -168
- package/dist/lib/devices/connect.js +6 -254
- package/dist/lib/devices/device-docs.d.ts +0 -24
- package/dist/lib/devices/device-docs.js +0 -46
- package/dist/lib/devices/discovery-policy.d.ts +0 -30
- package/dist/lib/devices/discovery-policy.js +3 -34
- package/dist/lib/devices/doctor-findings.d.ts +0 -109
- package/dist/lib/devices/doctor-findings.js +30 -326
- package/dist/lib/devices/doctor-overview-cache.d.ts +0 -33
- package/dist/lib/devices/doctor-overview-cache.js +0 -81
- package/dist/lib/devices/fleet-divergence.d.ts +0 -89
- package/dist/lib/devices/fleet-divergence.js +1 -60
- package/dist/lib/devices/fleet-inventory.d.ts +0 -27
- package/dist/lib/devices/fleet-inventory.js +0 -38
- package/dist/lib/devices/fleet.d.ts +0 -95
- package/dist/lib/devices/fleet.js +0 -79
- package/dist/lib/devices/harness-inventory.d.ts +0 -75
- package/dist/lib/devices/harness-inventory.js +0 -101
- package/dist/lib/devices/health-report.d.ts +0 -37
- package/dist/lib/devices/health-report.js +0 -73
- package/dist/lib/devices/health.d.ts +0 -71
- package/dist/lib/devices/health.js +2 -82
- package/dist/lib/devices/interactive-host.d.ts +0 -10
- package/dist/lib/devices/interactive-host.js +0 -34
- package/dist/lib/devices/known-hosts.d.ts +0 -45
- package/dist/lib/devices/known-hosts.js +0 -77
- package/dist/lib/devices/pending.d.ts +0 -31
- package/dist/lib/devices/pending.js +7 -67
- package/dist/lib/devices/pool.d.ts +0 -36
- package/dist/lib/devices/pool.js +0 -30
- package/dist/lib/devices/reachability.d.ts +0 -25
- package/dist/lib/devices/reachability.js +0 -14
- package/dist/lib/devices/registry.d.ts +0 -141
- package/dist/lib/devices/registry.js +4 -151
- package/dist/lib/devices/resolve-profile.d.ts +0 -18
- package/dist/lib/devices/resolve-profile.js +0 -18
- package/dist/lib/devices/resolve-target.d.ts +0 -20
- package/dist/lib/devices/resolve-target.js +1 -51
- package/dist/lib/devices/rollout-verify.d.ts +1 -81
- package/dist/lib/devices/rollout-verify.js +0 -77
- package/dist/lib/devices/self-host.d.ts +0 -7
- package/dist/lib/devices/self-host.js +1 -29
- package/dist/lib/devices/ssh-config.d.ts +0 -18
- package/dist/lib/devices/ssh-config.js +0 -9
- package/dist/lib/devices/stats-cache.d.ts +0 -48
- package/dist/lib/devices/stats-cache.js +0 -80
- package/dist/lib/devices/sync.d.ts +0 -97
- package/dist/lib/devices/sync.js +0 -88
- package/dist/lib/devices/tailscale.d.ts +0 -18
- package/dist/lib/devices/tailscale.js +1 -38
- package/dist/lib/devices/terminfo.d.ts +0 -27
- package/dist/lib/devices/terminfo.js +2 -64
- package/dist/lib/devices/worker-pick.d.ts +0 -15
- package/dist/lib/devices/worker-pick.js +0 -48
- package/dist/lib/diff-text.d.ts +0 -18
- package/dist/lib/diff-text.js +0 -15
- package/dist/lib/doctor-diff.d.ts +0 -79
- package/dist/lib/doctor-diff.js +2 -181
- package/dist/lib/drift-sync.d.ts +0 -29
- package/dist/lib/drift-sync.js +0 -40
- package/dist/lib/drift.d.ts +0 -43
- package/dist/lib/drift.js +0 -31
- package/dist/lib/event-families.d.ts +0 -17
- package/dist/lib/event-families.js +0 -33
- package/dist/lib/event-provenance.d.ts +0 -4
- package/dist/lib/event-provenance.js +0 -5
- package/dist/lib/event-stream.d.ts +0 -29
- package/dist/lib/event-stream.js +0 -36
- package/dist/lib/events-ingest.d.ts +0 -27
- package/dist/lib/events-ingest.js +0 -54
- package/dist/lib/exec-account-home.d.ts +0 -24
- package/dist/lib/exec-account-home.js +0 -41
- package/dist/lib/exec-bounded.d.ts +1 -37
- package/dist/lib/exec-bounded.js +10 -46
- package/dist/lib/exec.d.ts +3 -583
- package/dist/lib/exec.js +18 -1025
- package/dist/lib/factory/snapshot.js +1 -15
- package/dist/lib/feed/activity-stream.d.ts +0 -27
- package/dist/lib/feed/activity-stream.js +4 -80
- package/dist/lib/feed/activity.d.ts +1 -243
- package/dist/lib/feed/activity.js +8 -273
- package/dist/lib/feed/answer.d.ts +0 -103
- package/dist/lib/feed/answer.js +0 -267
- package/dist/lib/feed/attention.d.ts +0 -56
- package/dist/lib/feed/attention.js +0 -161
- package/dist/lib/feed/envelope.d.ts +2 -17
- package/dist/lib/feed/envelope.js +0 -14
- package/dist/lib/feed/events.d.ts +0 -118
- package/dist/lib/feed/events.js +13 -213
- package/dist/lib/feed/feed.d.ts +1 -250
- package/dist/lib/feed/feed.js +24 -334
- package/dist/lib/feed/hub-server.d.ts +0 -100
- package/dist/lib/feed/hub-server.js +1 -191
- package/dist/lib/feed/hub.d.ts +0 -53
- package/dist/lib/feed/hub.js +0 -89
- package/dist/lib/feed/pr-status.d.ts +1 -19
- package/dist/lib/feed/pr-status.js +0 -18
- package/dist/lib/feed/tool-activity.d.ts +0 -59
- package/dist/lib/feed/tool-activity.js +1 -119
- package/dist/lib/feed/tools.d.ts +0 -100
- package/dist/lib/feed/tools.js +0 -89
- package/dist/lib/feed/watch.d.ts +0 -33
- package/dist/lib/feed/watch.js +2 -94
- package/dist/lib/feed-broadcast.d.ts +0 -148
- package/dist/lib/feed-broadcast.js +1 -283
- package/dist/lib/feed-outcome.d.ts +0 -50
- package/dist/lib/feed-outcome.js +1 -67
- package/dist/lib/feed-policy.d.ts +0 -6
- package/dist/lib/feed-policy.js +1 -27
- package/dist/lib/feed-post.d.ts +0 -61
- package/dist/lib/feed-post.js +4 -61
- package/dist/lib/feed-ranking.d.ts +1 -1
- package/dist/lib/fleet/apply.d.ts +0 -64
- package/dist/lib/fleet/apply.js +0 -125
- package/dist/lib/fleet/auth-sync.d.ts +0 -48
- package/dist/lib/fleet/auth-sync.js +4 -49
- package/dist/lib/fleet/capture.d.ts +0 -26
- package/dist/lib/fleet/capture.js +1 -36
- package/dist/lib/fleet/manifest.d.ts +0 -38
- package/dist/lib/fleet/manifest.js +0 -40
- package/dist/lib/fleet/types.d.ts +1 -138
- package/dist/lib/fleet/types.js +0 -12
- package/dist/lib/fleet-cache.d.ts +0 -12
- package/dist/lib/fleet-cache.js +0 -24
- package/dist/lib/fleet-shared-state.d.ts +6 -59
- package/dist/lib/fleet-shared-state.js +5 -51
- package/dist/lib/fleet-status.d.ts +0 -27
- package/dist/lib/fleet-status.js +0 -48
- package/dist/lib/format.d.ts +0 -74
- package/dist/lib/format.js +3 -77
- package/dist/lib/friction-heuristics.d.ts +0 -20
- package/dist/lib/friction-heuristics.js +0 -8
- package/dist/lib/fs-atomic.d.ts +0 -51
- package/dist/lib/fs-atomic.js +8 -66
- package/dist/lib/fs-walk.d.ts +0 -13
- package/dist/lib/fs-walk.js +0 -22
- package/dist/lib/fuzzy.d.ts +0 -29
- package/dist/lib/fuzzy.js +0 -31
- package/dist/lib/gemini-settings.js +0 -12
- package/dist/lib/git.d.ts +0 -366
- package/dist/lib/git.js +7 -574
- package/dist/lib/github/ci-failure.d.ts +30 -0
- package/dist/lib/github/ci-failure.js +95 -0
- package/dist/lib/github/gh-overload.d.ts +0 -38
- package/dist/lib/github/gh-overload.js +3 -55
- package/dist/lib/github/pr-mergeable.d.ts +3 -34
- package/dist/lib/github/pr-mergeable.js +2 -42
- package/dist/lib/github/pr-verdict.d.ts +0 -44
- package/dist/lib/github/pr-verdict.js +0 -36
- package/dist/lib/github/project-prs.d.ts +135 -105
- package/dist/lib/github/project-prs.js +530 -170
- package/dist/lib/github/release-drift.d.ts +45 -0
- package/dist/lib/github/release-drift.js +91 -0
- package/dist/lib/github/rest.d.ts +0 -50
- package/dist/lib/github/rest.js +0 -49
- package/dist/lib/github/viewer.d.ts +27 -0
- package/dist/lib/github/viewer.js +101 -0
- package/dist/lib/goose-commands.d.ts +0 -29
- package/dist/lib/goose-commands.js +3 -48
- package/dist/lib/harness/adapter.d.ts +0 -151
- package/dist/lib/harness/adapter.js +0 -41
- package/dist/lib/harness/adapters/claude.d.ts +0 -8
- package/dist/lib/harness/adapters/claude.js +0 -89
- package/dist/lib/harness/adapters/codex.js +0 -18
- package/dist/lib/harness/adapters/copilot.js +0 -3
- package/dist/lib/harness/adapters/cursor.js +0 -16
- package/dist/lib/harness/adapters/droid.js +0 -3
- package/dist/lib/harness/adapters/kimi.js +0 -19
- package/dist/lib/harness/adapters/muse.js +0 -9
- package/dist/lib/harness/adapters/opencode.js +0 -2
- package/dist/lib/harness/exec-config-version.d.ts +0 -2
- package/dist/lib/harness/exec-config-version.js +0 -13
- package/dist/lib/harness/index.js +0 -10
- package/dist/lib/harness-auth-capabilities.d.ts +0 -26
- package/dist/lib/harness-auth-capabilities.js +0 -18
- package/dist/lib/harness-connection-test.d.ts +0 -27
- package/dist/lib/harness-connection-test.js +2 -20
- package/dist/lib/heal.d.ts +0 -45
- package/dist/lib/heal.js +2 -82
- package/dist/lib/help.d.ts +0 -32
- package/dist/lib/help.js +0 -41
- package/dist/lib/helper-download.d.ts +0 -85
- package/dist/lib/helper-download.js +0 -95
- package/dist/lib/helper-versions.d.ts +0 -51
- package/dist/lib/helper-versions.js +1 -45
- package/dist/lib/hooks/cache.d.ts +0 -40
- package/dist/lib/hooks/cache.js +8 -122
- package/dist/lib/hooks/install.d.ts +0 -253
- package/dist/lib/hooks/install.js +49 -822
- package/dist/lib/hooks/match.d.ts +0 -24
- package/dist/lib/hooks/match.js +0 -31
- package/dist/lib/hooks/profile.d.ts +0 -21
- package/dist/lib/hooks/profile.js +0 -28
- package/dist/lib/hosts/credential-transport.d.ts +0 -6
- package/dist/lib/hosts/credential-transport.js +0 -17
- package/dist/lib/hosts/credentials.d.ts +0 -35
- package/dist/lib/hosts/credentials.js +0 -38
- package/dist/lib/hosts/dispatch.d.ts +0 -140
- package/dist/lib/hosts/dispatch.js +0 -176
- package/dist/lib/hosts/logs.d.ts +0 -25
- package/dist/lib/hosts/logs.js +1 -51
- package/dist/lib/hosts/option.d.ts +0 -12
- package/dist/lib/hosts/option.js +0 -12
- package/dist/lib/hosts/passthrough.d.ts +0 -129
- package/dist/lib/hosts/passthrough.js +12 -249
- package/dist/lib/hosts/progress.d.ts +0 -90
- package/dist/lib/hosts/progress.js +5 -115
- package/dist/lib/hosts/providers/devices.d.ts +0 -19
- package/dist/lib/hosts/providers/devices.js +1 -34
- package/dist/lib/hosts/providers/local.d.ts +0 -14
- package/dist/lib/hosts/providers/local.js +0 -22
- package/dist/lib/hosts/ready.d.ts +0 -99
- package/dist/lib/hosts/ready.js +0 -101
- package/dist/lib/hosts/reconcile.d.ts +0 -38
- package/dist/lib/hosts/reconcile.js +1 -50
- package/dist/lib/hosts/reconnect.d.ts +0 -75
- package/dist/lib/hosts/reconnect.js +0 -98
- package/dist/lib/hosts/registry.d.ts +0 -74
- package/dist/lib/hosts/registry.js +0 -110
- package/dist/lib/hosts/remote-cmd.d.ts +1 -228
- package/dist/lib/hosts/remote-cmd.js +19 -294
- package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +0 -1
- package/dist/lib/hosts/remote-cmd.test-fixture.js +0 -12
- package/dist/lib/hosts/remote-os.d.ts +0 -19
- package/dist/lib/hosts/remote-os.js +0 -22
- package/dist/lib/hosts/remote-session-id.d.ts +0 -22
- package/dist/lib/hosts/remote-session-id.js +0 -24
- package/dist/lib/hosts/routing-flag.d.ts +0 -21
- package/dist/lib/hosts/routing-flag.js +0 -21
- package/dist/lib/hosts/run-target.d.ts +0 -53
- package/dist/lib/hosts/run-target.js +0 -56
- package/dist/lib/hosts/session-index.d.ts +1 -53
- package/dist/lib/hosts/session-index.js +2 -59
- package/dist/lib/hosts/session-marker.d.ts +0 -31
- package/dist/lib/hosts/session-marker.js +0 -32
- package/dist/lib/hosts/ssh-config.d.ts +0 -25
- package/dist/lib/hosts/ssh-config.js +1 -32
- package/dist/lib/hosts/tasks.d.ts +0 -54
- package/dist/lib/hosts/tasks.js +0 -35
- package/dist/lib/hosts/types.d.ts +0 -39
- package/dist/lib/hosts/types.js +0 -22
- package/dist/lib/humans.d.ts +0 -21
- package/dist/lib/humans.js +0 -30
- package/dist/lib/identity/client.d.ts +1 -35
- package/dist/lib/identity/client.js +3 -29
- package/dist/lib/identity/index.d.ts +3 -27
- package/dist/lib/identity/index.js +8 -24
- package/dist/lib/import.d.ts +0 -104
- package/dist/lib/import.js +1 -137
- package/dist/lib/installations/active-check.d.ts +0 -53
- package/dist/lib/installations/active-check.js +0 -49
- package/dist/lib/installations/index.d.ts +0 -9
- package/dist/lib/installations/index.js +0 -9
- package/dist/lib/installations/installation-lock.js +0 -8
- package/dist/lib/installations/launch-gate.d.ts +0 -56
- package/dist/lib/installations/launch-gate.js +0 -83
- package/dist/lib/installations/migrate.d.ts +0 -100
- package/dist/lib/installations/migrate.js +105 -572
- package/dist/lib/installations/native-command.d.ts +0 -1
- package/dist/lib/installations/native-command.js +0 -6
- package/dist/lib/installations/resolve.d.ts +0 -24
- package/dist/lib/installations/resolve.js +0 -24
- package/dist/lib/installations/shims.d.ts +0 -395
- package/dist/lib/installations/shims.js +30 -957
- package/dist/lib/installations/store.d.ts +0 -199
- package/dist/lib/installations/store.js +1 -297
- package/dist/lib/installations/strategies.d.ts +0 -44
- package/dist/lib/installations/strategies.js +0 -87
- package/dist/lib/installations/types.d.ts +1 -67
- package/dist/lib/installations/types.js +0 -6
- package/dist/lib/installations/update-cancellation.d.ts +0 -74
- package/dist/lib/installations/update-cancellation.js +0 -74
- package/dist/lib/installations/update-policy.d.ts +0 -57
- package/dist/lib/installations/update-policy.js +0 -62
- package/dist/lib/installations/update-runtime.d.ts +0 -88
- package/dist/lib/installations/update-runtime.js +1 -127
- package/dist/lib/installations/update.d.ts +0 -53
- package/dist/lib/installations/update.js +0 -95
- package/dist/lib/installations/versions.d.ts +0 -310
- package/dist/lib/installations/versions.js +14 -837
- package/dist/lib/isolation-boundary-report.d.ts +0 -7
- package/dist/lib/isolation-boundary-report.js +0 -7
- package/dist/lib/launch-identity.d.ts +0 -8
- package/dist/lib/launch-identity.js +0 -8
- package/dist/lib/linear-autoclose.d.ts +0 -25
- package/dist/lib/linear-autoclose.js +0 -19
- package/dist/lib/linear-cache.d.ts +0 -57
- package/dist/lib/linear-cache.js +1 -81
- package/dist/lib/linear-project-counts.d.ts +0 -83
- package/dist/lib/linear-project-counts.js +2 -64
- package/dist/lib/linear-projects.d.ts +0 -62
- package/dist/lib/linear-projects.js +0 -85
- package/dist/lib/linear-rate-limit.d.ts +0 -21
- package/dist/lib/linear-rate-limit.js +1 -93
- package/dist/lib/linux-userns.d.ts +1 -42
- package/dist/lib/linux-userns.js +0 -47
- package/dist/lib/lock-compromise.d.ts +0 -7
- package/dist/lib/lock-compromise.js +0 -7
- package/dist/lib/log-follow.d.ts +0 -2
- package/dist/lib/log-follow.js +5 -15
- package/dist/lib/loop.d.ts +0 -106
- package/dist/lib/loop.js +0 -135
- package/dist/lib/machine-id.d.ts +0 -19
- package/dist/lib/machine-id.js +0 -19
- package/dist/lib/mailbox-gc.d.ts +0 -7
- package/dist/lib/mailbox-gc.js +0 -25
- package/dist/lib/mailbox-target.d.ts +0 -31
- package/dist/lib/mailbox-target.js +0 -27
- package/dist/lib/mailbox.d.ts +0 -77
- package/dist/lib/mailbox.js +5 -105
- package/dist/lib/manifest.d.ts +0 -4
- package/dist/lib/manifest.js +0 -36
- package/dist/lib/markdown.d.ts +0 -3
- package/dist/lib/markdown.js +0 -9
- package/dist/lib/mcp-registry.d.ts +1 -50
- package/dist/lib/mcp-registry.js +0 -64
- package/dist/lib/mcp.d.ts +0 -115
- package/dist/lib/mcp.js +3 -199
- package/dist/lib/memory-cache.d.ts +0 -11
- package/dist/lib/memory-cache.js +0 -9
- package/dist/lib/memory.d.ts +0 -52
- package/dist/lib/memory.js +12 -74
- package/dist/lib/menubar/download-menubar.d.ts +0 -35
- package/dist/lib/menubar/download-menubar.js +0 -38
- package/dist/lib/menubar/install-menubar.d.ts +0 -405
- package/dist/lib/menubar/install-menubar.js +25 -667
- package/dist/lib/menubar/migrate-prefs.d.ts +0 -47
- package/dist/lib/menubar/migrate-prefs.js +1 -70
- package/dist/lib/menubar/notify-desktop.d.ts +0 -82
- package/dist/lib/menubar/notify-desktop.js +0 -68
- package/dist/lib/menubar/resolve-version.d.ts +0 -41
- package/dist/lib/menubar/resolve-version.js +2 -44
- package/dist/lib/menubar/snapshot.d.ts +13 -66
- package/dist/lib/menubar/snapshot.js +45 -39
- package/dist/lib/merged-resources.d.ts +0 -10
- package/dist/lib/merged-resources.js +0 -10
- package/dist/lib/migrate-fold.d.ts +0 -31
- package/dist/lib/migrate-fold.js +6 -38
- package/dist/lib/model-tier-overrides.d.ts +0 -30
- package/dist/lib/model-tier-overrides.js +0 -14
- package/dist/lib/model-tiers.d.ts +0 -43
- package/dist/lib/model-tiers.js +5 -78
- package/dist/lib/models.d.ts +0 -145
- package/dist/lib/models.js +3 -333
- package/dist/lib/monitors/config.d.ts +0 -187
- package/dist/lib/monitors/config.js +4 -101
- package/dist/lib/monitors/dispatch.d.ts +0 -22
- package/dist/lib/monitors/dispatch.js +0 -35
- package/dist/lib/monitors/engine.d.ts +1 -53
- package/dist/lib/monitors/engine.js +17 -106
- package/dist/lib/monitors/fingerprint.d.ts +0 -43
- package/dist/lib/monitors/fingerprint.js +0 -57
- package/dist/lib/monitors/pid-watch.d.ts +0 -31
- package/dist/lib/monitors/pid-watch.js +0 -31
- package/dist/lib/monitors/remote.d.ts +0 -52
- package/dist/lib/monitors/remote.js +0 -42
- package/dist/lib/monitors/sources/command.d.ts +2 -9
- package/dist/lib/monitors/sources/command.js +13 -34
- package/dist/lib/monitors/sources/device.d.ts +0 -10
- package/dist/lib/monitors/sources/device.js +0 -14
- package/dist/lib/monitors/sources/failure.d.ts +0 -27
- package/dist/lib/monitors/sources/failure.js +0 -27
- package/dist/lib/monitors/sources/file.d.ts +0 -10
- package/dist/lib/monitors/sources/file.js +0 -11
- package/dist/lib/monitors/sources/http.d.ts +0 -7
- package/dist/lib/monitors/sources/http.js +0 -9
- package/dist/lib/monitors/sources/index.d.ts +0 -9
- package/dist/lib/monitors/sources/index.js +0 -9
- package/dist/lib/monitors/sources/poll.d.ts +0 -8
- package/dist/lib/monitors/sources/poll.js +0 -8
- package/dist/lib/monitors/sources/types.d.ts +0 -18
- package/dist/lib/monitors/sources/types.js +0 -8
- package/dist/lib/monitors/sources/webhook.d.ts +0 -7
- package/dist/lib/monitors/sources/webhook.js +0 -7
- package/dist/lib/monitors/sources/ws.d.ts +0 -10
- package/dist/lib/monitors/sources/ws.js +3 -13
- package/dist/lib/monitors/state.d.ts +0 -132
- package/dist/lib/monitors/state.js +2 -86
- package/dist/lib/native-accounts.d.ts +0 -32
- package/dist/lib/native-accounts.js +0 -17
- package/dist/lib/net-close.d.ts +0 -9
- package/dist/lib/net-close.js +0 -5
- package/dist/lib/notify.d.ts +0 -47
- package/dist/lib/notify.js +0 -30
- package/dist/lib/onepassword.d.ts +0 -14
- package/dist/lib/onepassword.js +0 -12
- package/dist/lib/open-url.d.ts +0 -38
- package/dist/lib/open-url.js +1 -69
- package/dist/lib/openclaw-keychain.js +2 -7
- package/dist/lib/operator.d.ts +0 -9
- package/dist/lib/operator.js +0 -34
- package/dist/lib/origin-machine.d.ts +0 -16
- package/dist/lib/origin-machine.js +0 -16
- package/dist/lib/output/git-output.d.ts +0 -31
- package/dist/lib/output/git-output.js +4 -43
- package/dist/lib/overdue.d.ts +0 -35
- package/dist/lib/overdue.js +0 -60
- package/dist/lib/owner-message.d.ts +0 -20
- package/dist/lib/owner-message.js +1 -40
- package/dist/lib/packages/output-home.d.ts +0 -28
- package/dist/lib/packages/output-home.js +0 -114
- package/dist/lib/paths.d.ts +0 -26
- package/dist/lib/paths.js +1 -27
- package/dist/lib/percentile.d.ts +0 -11
- package/dist/lib/percentile.js +0 -11
- package/dist/lib/perf/db.d.ts +0 -16
- package/dist/lib/perf/db.js +3 -32
- package/dist/lib/perf/spool.d.ts +0 -12
- package/dist/lib/perf/spool.js +0 -13
- package/dist/lib/perf/types.d.ts +0 -26
- package/dist/lib/perf/types.js +0 -1
- package/dist/lib/permissions-registry.d.ts +0 -48
- package/dist/lib/permissions-registry.js +0 -108
- package/dist/lib/permissions.d.ts +0 -215
- package/dist/lib/permissions.js +4 -407
- package/dist/lib/picker.d.ts +0 -124
- package/dist/lib/picker.js +0 -140
- package/dist/lib/placement.d.ts +0 -57
- package/dist/lib/placement.js +0 -47
- package/dist/lib/platform/exec.d.ts +0 -73
- package/dist/lib/platform/exec.js +0 -80
- package/dist/lib/platform/index.d.ts +0 -14
- package/dist/lib/platform/index.js +0 -14
- package/dist/lib/platform/ipc.d.ts +0 -10
- package/dist/lib/platform/ipc.js +0 -13
- package/dist/lib/platform/links.d.ts +0 -14
- package/dist/lib/platform/links.js +0 -25
- package/dist/lib/platform/paths.d.ts +0 -48
- package/dist/lib/platform/paths.js +0 -52
- package/dist/lib/platform/posixpath.d.ts +0 -18
- package/dist/lib/platform/posixpath.js +0 -62
- package/dist/lib/platform/process.d.ts +0 -67
- package/dist/lib/platform/process.js +5 -99
- package/dist/lib/platform/winpath.d.ts +0 -50
- package/dist/lib/platform/winpath.js +0 -81
- package/dist/lib/plugins/plugin-marketplace.d.ts +0 -165
- package/dist/lib/plugins/plugin-marketplace.js +10 -238
- package/dist/lib/plugins/plugins.d.ts +0 -238
- package/dist/lib/plugins/plugins.js +20 -454
- package/dist/lib/plugins/skills.d.ts +0 -86
- package/dist/lib/plugins/skills.js +1 -139
- package/dist/lib/pr-land-detach.d.ts +0 -17
- package/dist/lib/pr-land-detach.js +0 -30
- package/dist/lib/pricing/cost.d.ts +0 -29
- package/dist/lib/pricing/cost.js +0 -38
- package/dist/lib/pricing/index.d.ts +0 -6
- package/dist/lib/pricing/index.js +0 -6
- package/dist/lib/pricing/table.d.ts +0 -8
- package/dist/lib/pricing/table.js +3 -56
- package/dist/lib/probe.d.ts +0 -5
- package/dist/lib/probe.js +0 -40
- package/dist/lib/profiles-presets.d.ts +0 -20
- package/dist/lib/profiles-presets.js +1 -36
- package/dist/lib/profiles.d.ts +0 -185
- package/dist/lib/profiles.js +1 -158
- package/dist/lib/project-doctor.d.ts +0 -29
- package/dist/lib/project-doctor.js +0 -26
- package/dist/lib/project-focus.d.ts +0 -41
- package/dist/lib/project-focus.js +0 -49
- package/dist/lib/project-import.d.ts +0 -41
- package/dist/lib/project-import.js +0 -36
- package/dist/lib/project-key.d.ts +0 -65
- package/dist/lib/project-key.js +0 -70
- package/dist/lib/project-launch.d.ts +0 -49
- package/dist/lib/project-launch.js +2 -91
- package/dist/lib/project-probe.d.ts +0 -86
- package/dist/lib/project-probe.js +1 -88
- package/dist/lib/project-pull.d.ts +0 -96
- package/dist/lib/project-pull.js +0 -122
- package/dist/lib/project-resources.d.ts +0 -44
- package/dist/lib/project-resources.js +7 -171
- package/dist/lib/project-root.d.ts +0 -95
- package/dist/lib/project-root.js +0 -101
- package/dist/lib/project-schedule.d.ts +0 -43
- package/dist/lib/project-schedule.js +0 -52
- package/dist/lib/project-status.d.ts +0 -108
- package/dist/lib/project-status.js +0 -117
- package/dist/lib/projects.d.ts +0 -197
- package/dist/lib/projects.js +1 -213
- package/dist/lib/pwsh.d.ts +0 -41
- package/dist/lib/pwsh.js +0 -51
- package/dist/lib/quick-todo.d.ts +84 -0
- package/dist/lib/quick-todo.js +243 -0
- package/dist/lib/reconcile-and-repair.d.ts +0 -78
- package/dist/lib/reconcile-and-repair.js +3 -66
- package/dist/lib/redact.d.ts +0 -29
- package/dist/lib/redact.js +0 -54
- package/dist/lib/refresh-coordinator.d.ts +0 -15
- package/dist/lib/refresh-coordinator.js +0 -10
- package/dist/lib/refresh.d.ts +0 -39
- package/dist/lib/refresh.js +0 -45
- package/dist/lib/registry.d.ts +0 -65
- package/dist/lib/registry.js +0 -127
- package/dist/lib/reminders.d.ts +0 -21
- package/dist/lib/reminders.js +0 -30
- package/dist/lib/remote-agents-json.d.ts +0 -44
- package/dist/lib/remote-agents-json.js +1 -44
- package/dist/lib/reserved-stores.d.ts +0 -28
- package/dist/lib/reserved-stores.js +2 -28
- package/dist/lib/resource-aliases.d.ts +0 -12
- package/dist/lib/resource-aliases.js +0 -12
- package/dist/lib/resource-content-diff.d.ts +0 -28
- package/dist/lib/resource-content-diff.js +7 -34
- package/dist/lib/resource-inventory.d.ts +0 -51
- package/dist/lib/resource-inventory.js +0 -45
- package/dist/lib/resource-patterns.d.ts +0 -32
- package/dist/lib/resource-patterns.js +0 -34
- package/dist/lib/resource-profiles.d.ts +0 -7
- package/dist/lib/resource-profiles.js +0 -11
- package/dist/lib/resources/types.d.ts +0 -50
- package/dist/lib/resources/types.js +0 -7
- package/dist/lib/resources/workflows.d.ts +0 -7
- package/dist/lib/resources/workflows.js +0 -13
- package/dist/lib/resources.d.ts +0 -65
- package/dist/lib/resources.js +0 -78
- package/dist/lib/routers.d.ts +0 -67
- package/dist/lib/routers.js +0 -60
- package/dist/lib/routine-activation.d.ts +0 -23
- package/dist/lib/routine-activation.js +0 -24
- package/dist/lib/routine-context.d.ts +1 -91
- package/dist/lib/routine-context.js +0 -107
- package/dist/lib/routine-notify-owner.d.ts +0 -79
- package/dist/lib/routine-notify-owner.js +1 -103
- package/dist/lib/routine-notify.d.ts +0 -70
- package/dist/lib/routine-notify.js +1 -75
- package/dist/lib/routine-process-cleanup.d.ts +0 -1
- package/dist/lib/routine-process-cleanup.js +1 -4
- package/dist/lib/routine-readiness.d.ts +0 -63
- package/dist/lib/routine-readiness.js +4 -86
- package/dist/lib/routines-format.d.ts +0 -40
- package/dist/lib/routines-format.js +0 -67
- package/dist/lib/routines-placement.d.ts +0 -33
- package/dist/lib/routines-placement.js +0 -40
- package/dist/lib/routines-project.d.ts +0 -57
- package/dist/lib/routines-project.js +6 -80
- package/dist/lib/rules/compile.d.ts +0 -66
- package/dist/lib/rules/compile.js +6 -118
- package/dist/lib/rules/compose.d.ts +0 -56
- package/dist/lib/rules/compose.js +2 -83
- package/dist/lib/rules/rules.d.ts +0 -23
- package/dist/lib/rules/rules.js +0 -35
- package/dist/lib/rules/run-sync.d.ts +0 -16
- package/dist/lib/rules/run-sync.js +1 -44
- package/dist/lib/run-config.d.ts +0 -7
- package/dist/lib/run-config.js +0 -8
- package/dist/lib/run-defaults.d.ts +0 -14
- package/dist/lib/run-defaults.js +0 -14
- package/dist/lib/run-notify.d.ts +0 -18
- package/dist/lib/run-notify.js +0 -36
- package/dist/lib/run-trace-sync.d.ts +0 -21
- package/dist/lib/run-trace-sync.js +2 -66
- package/dist/lib/rush-session.d.ts +0 -26
- package/dist/lib/rush-session.js +0 -28
- package/dist/lib/sandbox.d.ts +0 -45
- package/dist/lib/sandbox.js +5 -93
- package/dist/lib/scheduler.d.ts +0 -28
- package/dist/lib/scheduler.js +0 -45
- package/dist/lib/scheduling/routines.d.ts +0 -652
- package/dist/lib/scheduling/routines.js +8 -475
- package/dist/lib/secrets-cli.d.ts +3 -8
- package/dist/lib/secrets-cli.js +1 -19
- package/dist/lib/secrets-client.d.ts +0 -117
- package/dist/lib/secrets-client.js +4 -275
- package/dist/lib/secrets-policy.d.ts +0 -142
- package/dist/lib/secrets-policy.js +4 -237
- package/dist/lib/secrets-types.d.ts +0 -87
- package/dist/lib/secrets-types.js +0 -13
- package/dist/lib/self-heal/checks/hook-manifest.js +0 -25
- package/dist/lib/self-heal/checks/hook-runtime.js +0 -5
- package/dist/lib/self-heal/checks/install-staging.d.ts +0 -1
- package/dist/lib/self-heal/checks/install-staging.js +1 -28
- package/dist/lib/self-heal/checks/menubar-helper.js +0 -5
- package/dist/lib/self-heal/checks/path.js +0 -9
- package/dist/lib/self-heal/checks/resources.js +0 -5
- package/dist/lib/self-heal/checks/shadowing.js +2 -16
- package/dist/lib/self-heal/checks/shims.js +0 -14
- package/dist/lib/self-heal/child.d.ts +12 -0
- package/dist/lib/self-heal/child.js +23 -0
- package/dist/lib/self-heal/registry.d.ts +0 -9
- package/dist/lib/self-heal/registry.js +0 -16
- package/dist/lib/self-heal/types.d.ts +0 -12
- package/dist/lib/self-heal/types.js +0 -15
- package/dist/lib/self-update.d.ts +0 -312
- package/dist/lib/self-update.js +9 -360
- package/dist/lib/service-manifest.d.ts +0 -39
- package/dist/lib/service-manifest.js +0 -72
- package/dist/lib/session/active.d.ts +13 -869
- package/dist/lib/session/active.js +17 -976
- package/dist/lib/session/actor-sidecar.d.ts +1 -32
- package/dist/lib/session/actor-sidecar.js +1 -41
- package/dist/lib/session/bookmarks.d.ts +0 -33
- package/dist/lib/session/bookmarks.js +0 -37
- package/dist/lib/session/bundle.d.ts +0 -86
- package/dist/lib/session/bundle.js +0 -54
- package/dist/lib/session/claude-accounts.d.ts +0 -40
- package/dist/lib/session/claude-accounts.js +0 -49
- package/dist/lib/session/cloud.d.ts +1 -23
- package/dist/lib/session/cloud.js +0 -26
- package/dist/lib/session/db.d.ts +5 -658
- package/dist/lib/session/db.js +22 -1443
- package/dist/lib/session/detached.d.ts +0 -21
- package/dist/lib/session/detached.js +0 -29
- package/dist/lib/session/discover.d.ts +3 -420
- package/dist/lib/session/discover.js +30 -1123
- package/dist/lib/session/fork.d.ts +1 -41
- package/dist/lib/session/fork.js +0 -19
- package/dist/lib/session/ghostty-tabs.d.ts +0 -24
- package/dist/lib/session/ghostty-tabs.js +0 -37
- package/dist/lib/session/glance-files.d.ts +19 -0
- package/dist/lib/session/glance-files.js +258 -0
- package/dist/lib/session/hook-sessions.d.ts +0 -33
- package/dist/lib/session/hook-sessions.js +4 -99
- package/dist/lib/session/host-link.d.ts +1 -94
- package/dist/lib/session/host-link.js +1 -81
- package/dist/lib/session/inject-target.d.ts +8 -0
- package/dist/lib/session/inject-target.js +32 -0
- package/dist/lib/session/inject.d.ts +0 -15
- package/dist/lib/session/inject.js +0 -15
- package/dist/lib/session/live-metadata.d.ts +1 -34
- package/dist/lib/session/live-metadata.js +1 -73
- package/dist/lib/session/local-tmux-attach.d.ts +0 -37
- package/dist/lib/session/local-tmux-attach.js +1 -76
- package/dist/lib/session/migrate-targets.d.ts +0 -46
- package/dist/lib/session/migrate-targets.js +0 -24
- package/dist/lib/session/migrations.d.ts +0 -12
- package/dist/lib/session/migrations.js +1 -20
- package/dist/lib/session/mirror.d.ts +0 -32
- package/dist/lib/session/mirror.js +0 -77
- package/dist/lib/session/origin-machine.js +0 -3
- package/dist/lib/session/pid-registry.d.ts +0 -80
- package/dist/lib/session/pid-registry.js +2 -81
- package/dist/lib/session/presence.d.ts +0 -39
- package/dist/lib/session/presence.js +2 -52
- package/dist/lib/session/process-view.d.ts +0 -11
- package/dist/lib/session/process-view.js +0 -25
- package/dist/lib/session/projection.d.ts +0 -3
- package/dist/lib/session/projection.js +0 -10
- package/dist/lib/session/provenance.d.ts +1 -37
- package/dist/lib/session/provenance.js +0 -69
- package/dist/lib/session/recovery.d.ts +1 -40
- package/dist/lib/session/recovery.js +2 -80
- package/dist/lib/session/relative-time.js +0 -3
- package/dist/lib/session/remote/peer-stream.d.ts +0 -42
- package/dist/lib/session/remote/peer-stream.js +0 -64
- package/dist/lib/session/remote/remote-bundle.d.ts +0 -6
- package/dist/lib/session/remote/remote-bundle.js +0 -21
- package/dist/lib/session/remote/remote-list.d.ts +1 -110
- package/dist/lib/session/remote/remote-list.js +5 -139
- package/dist/lib/session/remote/remote.d.ts +0 -98
- package/dist/lib/session/remote/remote.js +3 -119
- package/dist/lib/session/remote/watch.d.ts +1 -34
- package/dist/lib/session/remote/watch.js +12 -72
- package/dist/lib/session/remote-active.d.ts +0 -35
- package/dist/lib/session/remote-active.js +0 -48
- package/dist/lib/session/remote-bundle.js +0 -2
- package/dist/lib/session/remote-list.js +0 -2
- package/dist/lib/session/remote-preview-cache.d.ts +0 -7
- package/dist/lib/session/remote-preview-cache.js +1 -76
- package/dist/lib/session/remote.js +0 -2
- package/dist/lib/session/resume-command.d.ts +0 -6
- package/dist/lib/session/resume-command.js +0 -6
- package/dist/lib/session/resume-owner.d.ts +1 -52
- package/dist/lib/session/resume-owner.js +0 -56
- package/dist/lib/session/run-names.d.ts +0 -24
- package/dist/lib/session/run-names.js +0 -26
- package/dist/lib/session/session-cache.d.ts +2 -146
- package/dist/lib/session/session-cache.js +26 -203
- package/dist/lib/session/short-id.js +0 -3
- package/dist/lib/session/sync/agents.d.ts +0 -68
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/backend.d.ts +0 -39
- package/dist/lib/session/sync/backend.js +0 -45
- package/dist/lib/session/sync/config.d.ts +0 -27
- package/dist/lib/session/sync/config.js +0 -56
- package/dist/lib/session/sync/managed-config.d.ts +0 -17
- package/dist/lib/session/sync/managed-config.js +0 -17
- package/dist/lib/session/sync/managed-key.d.ts +0 -38
- package/dist/lib/session/sync/managed-key.js +1 -42
- package/dist/lib/session/sync/net-client.d.ts +0 -36
- package/dist/lib/session/sync/net-client.js +0 -30
- package/dist/lib/session/sync/provision.d.ts +0 -2
- package/dist/lib/session/sync/provision.js +0 -13
- package/dist/lib/session/sync/r2.d.ts +0 -20
- package/dist/lib/session/sync/r2.js +0 -20
- package/dist/lib/session/sync/transcript-crypto.d.ts +0 -37
- package/dist/lib/session/sync/transcript-crypto.js +5 -37
- package/dist/lib/session/sync/worker-template.d.ts +0 -5
- package/dist/lib/session/sync/worker-template.js +0 -40
- package/dist/lib/session/testdata/pid-registry-namespace.js +0 -2
- package/dist/lib/session/testdata/process-view-private.js +0 -1
- package/dist/lib/session/throughput.d.ts +0 -24
- package/dist/lib/session/throughput.js +1 -25
- package/dist/lib/session/timeline-pass.d.ts +1 -63
- package/dist/lib/session/timeline-pass.js +161 -148
- package/dist/lib/session/title.d.ts +0 -182
- package/dist/lib/session/title.js +0 -139
- package/dist/lib/session/tool-index.d.ts +2 -21
- package/dist/lib/session/tool-index.js +3 -75
- package/dist/lib/session/tool-store.d.ts +2 -48
- package/dist/lib/session/tool-store.js +1 -33
- package/dist/lib/session/viewing-in.d.ts +0 -67
- package/dist/lib/session/viewing-in.js +1 -71
- package/dist/lib/session/watch.js +0 -2
- package/dist/lib/session/width.js +0 -3
- package/dist/lib/sessions-client.d.ts +0 -46
- package/dist/lib/sessions-client.js +36 -113
- package/dist/lib/settings-manifest.d.ts +1 -17
- package/dist/lib/settings-manifest.js +4 -57
- package/dist/lib/setup-tool-install.d.ts +0 -1
- package/dist/lib/setup-tool-install.js +2 -7
- package/dist/lib/setup-tool-status.d.ts +0 -3
- package/dist/lib/setup-tool-status.js +4 -14
- package/dist/lib/sha256-asset.d.ts +0 -20
- package/dist/lib/sha256-asset.js +0 -20
- package/dist/lib/share-runtime.d.ts +0 -10
- package/dist/lib/share-runtime.js +1 -27
- package/dist/lib/shim-heal.d.ts +0 -15
- package/dist/lib/shim-heal.js +3 -29
- package/dist/lib/signin-badge.d.ts +0 -72
- package/dist/lib/signin-badge.js +0 -86
- package/dist/lib/sink-format.d.ts +0 -32
- package/dist/lib/sink-format.js +0 -14
- package/dist/lib/smart-launch.d.ts +0 -75
- package/dist/lib/smart-launch.js +1 -83
- package/dist/lib/snapshot.d.ts +0 -46
- package/dist/lib/snapshot.js +5 -31
- package/dist/lib/spinner.d.ts +0 -37
- package/dist/lib/spinner.js +0 -37
- package/dist/lib/sqlite.d.ts +0 -13
- package/dist/lib/sqlite.js +3 -71
- package/dist/lib/ssh-exec.d.ts +0 -209
- package/dist/lib/ssh-exec.js +11 -248
- package/dist/lib/ssh-tunnel.d.ts +0 -43
- package/dist/lib/ssh-tunnel.js +2 -48
- package/dist/lib/staleness/checkers/commands.d.ts +0 -4
- package/dist/lib/staleness/checkers/commands.js +0 -4
- package/dist/lib/staleness/checkers/hooks.d.ts +0 -10
- package/dist/lib/staleness/checkers/hooks.js +0 -15
- package/dist/lib/staleness/checkers/mcp.d.ts +0 -9
- package/dist/lib/staleness/checkers/mcp.js +0 -9
- package/dist/lib/staleness/checkers/permissions.d.ts +0 -12
- package/dist/lib/staleness/checkers/permissions.js +0 -13
- package/dist/lib/staleness/checkers/plugins.d.ts +0 -8
- package/dist/lib/staleness/checkers/plugins.js +0 -8
- package/dist/lib/staleness/checkers/rules.d.ts +0 -15
- package/dist/lib/staleness/checkers/rules.js +0 -26
- package/dist/lib/staleness/checkers/skills.d.ts +0 -4
- package/dist/lib/staleness/checkers/skills.js +0 -4
- package/dist/lib/staleness/checkers/subagents.d.ts +0 -9
- package/dist/lib/staleness/checkers/subagents.js +0 -9
- package/dist/lib/staleness/checkers/types.d.ts +0 -34
- package/dist/lib/staleness/checkers/types.js +0 -19
- package/dist/lib/staleness/checkers/workflows.d.ts +0 -7
- package/dist/lib/staleness/checkers/workflows.js +0 -7
- package/dist/lib/staleness/detectors/commands.js +0 -17
- package/dist/lib/staleness/detectors/hooks.js +1 -6
- package/dist/lib/staleness/detectors/mcp.js +0 -4
- package/dist/lib/staleness/detectors/permissions.js +10 -23
- package/dist/lib/staleness/detectors/rules.js +0 -6
- package/dist/lib/staleness/detectors/skills.d.ts +0 -1
- package/dist/lib/staleness/detectors/skills.js +1 -12
- package/dist/lib/staleness/detectors/types.d.ts +0 -10
- package/dist/lib/staleness/fingerprint.d.ts +0 -19
- package/dist/lib/staleness/fingerprint.js +0 -35
- package/dist/lib/staleness/index.d.ts +0 -28
- package/dist/lib/staleness/index.js +1 -45
- package/dist/lib/staleness/layers.d.ts +0 -23
- package/dist/lib/staleness/layers.js +0 -34
- package/dist/lib/staleness/prune.d.ts +0 -71
- package/dist/lib/staleness/prune.js +2 -27
- package/dist/lib/staleness/registry.d.ts +0 -10
- package/dist/lib/staleness/registry.js +3 -33
- package/dist/lib/staleness/types.d.ts +0 -36
- package/dist/lib/staleness/types.js +0 -5
- package/dist/lib/staleness/writers/commands.js +1 -48
- package/dist/lib/staleness/writers/hooks.js +1 -18
- package/dist/lib/staleness/writers/kinds.d.ts +0 -12
- package/dist/lib/staleness/writers/kinds.js +0 -1
- package/dist/lib/staleness/writers/lazy-map.d.ts +0 -11
- package/dist/lib/staleness/writers/mcp.js +0 -13
- package/dist/lib/staleness/writers/plugins.js +0 -4
- package/dist/lib/staleness/writers/rules.d.ts +0 -1
- package/dist/lib/staleness/writers/rules.js +1 -19
- package/dist/lib/staleness/writers/skills.js +3 -14
- package/dist/lib/staleness/writers/sources.d.ts +0 -11
- package/dist/lib/staleness/writers/sources.js +0 -32
- package/dist/lib/staleness/writers/subagents.js +0 -16
- package/dist/lib/staleness/writers/types.d.ts +0 -58
- package/dist/lib/staleness/writers/workflows.js +1 -1
- package/dist/lib/standalone-tools.d.ts +24 -0
- package/dist/lib/standalone-tools.js +58 -0
- package/dist/lib/star-nudge.d.ts +0 -32
- package/dist/lib/star-nudge.js +2 -36
- package/dist/lib/startup/command-registry.d.ts +0 -45
- package/dist/lib/startup/command-registry.js +1 -46
- package/dist/lib/startup/dev-build.d.ts +0 -9
- package/dist/lib/startup/dev-build.js +1 -42
- package/dist/lib/startup/root-command.d.ts +0 -19
- package/dist/lib/startup/root-command.js +0 -19
- package/dist/lib/startup/spellcheck.d.ts +0 -13
- package/dist/lib/startup/spellcheck.js +0 -13
- package/dist/lib/state.d.ts +0 -376
- package/dist/lib/state.js +19 -737
- package/dist/lib/storage/selection.d.ts +0 -40
- package/dist/lib/storage/selection.js +0 -29
- package/dist/lib/subagents-registry.d.ts +0 -56
- package/dist/lib/subagents-registry.js +0 -99
- package/dist/lib/subagents.d.ts +0 -128
- package/dist/lib/subagents.js +1 -158
- package/dist/lib/summarizer/config.d.ts +0 -37
- package/dist/lib/summarizer/config.js +0 -37
- package/dist/lib/summarizer/pass.d.ts +0 -20
- package/dist/lib/summarizer/pass.js +1 -34
- package/dist/lib/summarizer/summarize.d.ts +1 -49
- package/dist/lib/summarizer/summarize.js +0 -35
- package/dist/lib/sync-passphrase.d.ts +0 -16
- package/dist/lib/sync-passphrase.js +1 -18
- package/dist/lib/sync-status.d.ts +0 -72
- package/dist/lib/sync-status.js +0 -55
- package/dist/lib/sync-umbrella.d.ts +0 -54
- package/dist/lib/sync-umbrella.js +0 -48
- package/dist/lib/system-run-sync.d.ts +0 -5
- package/dist/lib/system-run-sync.js +1 -31
- package/dist/lib/teams/agents.d.ts +1 -444
- package/dist/lib/teams/agents.js +11 -871
- package/dist/lib/teams/api.d.ts +2 -52
- package/dist/lib/teams/api.js +4 -63
- package/dist/lib/teams/debug.d.ts +0 -6
- package/dist/lib/teams/debug.js +0 -6
- package/dist/lib/teams/delivery.d.ts +0 -41
- package/dist/lib/teams/delivery.js +0 -32
- package/dist/lib/teams/file_ops.d.ts +0 -11
- package/dist/lib/teams/file_ops.js +0 -11
- package/dist/lib/teams/forEach.d.ts +0 -76
- package/dist/lib/teams/forEach.js +1 -77
- package/dist/lib/teams/index.d.ts +1 -7
- package/dist/lib/teams/index.js +1 -7
- package/dist/lib/teams/parsers.d.ts +0 -4
- package/dist/lib/teams/parsers.js +0 -81
- package/dist/lib/teams/persistence.d.ts +0 -2
- package/dist/lib/teams/persistence.js +0 -12
- package/dist/lib/teams/placement-probe.d.ts +0 -12
- package/dist/lib/teams/placement-probe.js +1 -51
- package/dist/lib/teams/pr-watch.d.ts +0 -107
- package/dist/lib/teams/pr-watch.js +1 -107
- package/dist/lib/teams/registry.d.ts +0 -36
- package/dist/lib/teams/registry.js +0 -47
- package/dist/lib/teams/remoteWorktree.d.ts +0 -50
- package/dist/lib/teams/remoteWorktree.js +1 -96
- package/dist/lib/teams/scheduler.d.ts +0 -111
- package/dist/lib/teams/scheduler.js +2 -125
- package/dist/lib/teams/summarizer.d.ts +0 -22
- package/dist/lib/teams/summarizer.js +6 -50
- package/dist/lib/teams/supervisor.d.ts +0 -38
- package/dist/lib/teams/supervisor.js +0 -21
- package/dist/lib/teams/worktree.d.ts +0 -95
- package/dist/lib/teams/worktree.js +3 -110
- package/dist/lib/template.d.ts +0 -21
- package/dist/lib/template.js +0 -23
- package/dist/lib/term-client.d.ts +0 -26
- package/dist/lib/term-client.js +0 -48
- package/dist/lib/term-driver.d.ts +0 -5
- package/dist/lib/term-driver.js +0 -8
- package/dist/lib/terminal/backends/ghostty.d.ts +0 -2
- package/dist/lib/terminal/backends/ghostty.js +0 -12
- package/dist/lib/terminal/backends/index.d.ts +0 -17
- package/dist/lib/terminal/backends/index.js +0 -15
- package/dist/lib/terminal/backends/iterm.d.ts +0 -2
- package/dist/lib/terminal/backends/iterm.js +0 -10
- package/dist/lib/terminal/backends/terminal-app.d.ts +0 -10
- package/dist/lib/terminal/backends/terminal-app.js +0 -30
- package/dist/lib/terminal/backends/tmux.d.ts +0 -10
- package/dist/lib/terminal/backends/tmux.js +0 -2
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +0 -25
- package/dist/lib/terminal/backends/vscodium-agent.js +0 -36
- package/dist/lib/terminal/engine.d.ts +0 -17
- package/dist/lib/terminal/engine.js +0 -10
- package/dist/lib/terminal/index.d.ts +0 -8
- package/dist/lib/terminal/inject.d.ts +1 -173
- package/dist/lib/terminal/inject.js +0 -146
- package/dist/lib/terminal/policy.d.ts +0 -8
- package/dist/lib/terminal/policy.js +0 -1
- package/dist/lib/terminal/preferred.d.ts +0 -66
- package/dist/lib/terminal/preferred.js +0 -30
- package/dist/lib/terminal/quote.d.ts +0 -8
- package/dist/lib/terminal/quote.js +0 -8
- package/dist/lib/terminal/resolve.d.ts +0 -46
- package/dist/lib/terminal/resolve.js +0 -30
- package/dist/lib/terminal/run-surface.d.ts +0 -56
- package/dist/lib/terminal/run-surface.js +0 -46
- package/dist/lib/terminal/shell.d.ts +0 -3
- package/dist/lib/terminal/shell.js +0 -13
- package/dist/lib/terminal/transport.d.ts +0 -14
- package/dist/lib/terminal/transport.js +1 -30
- package/dist/lib/terminal/types.d.ts +0 -40
- package/dist/lib/terminal/types.js +0 -10
- package/dist/lib/text/relative-time.d.ts +0 -36
- package/dist/lib/text/relative-time.js +0 -43
- package/dist/lib/text/short-id.d.ts +0 -16
- package/dist/lib/text/short-id.js +0 -16
- package/dist/lib/text/width.d.ts +0 -24
- package/dist/lib/text/width.js +10 -52
- package/dist/lib/tmux/binary.d.ts +0 -36
- package/dist/lib/tmux/binary.js +0 -28
- package/dist/lib/tmux/index.d.ts +0 -5
- package/dist/lib/tmux/index.js +0 -5
- package/dist/lib/tmux/orphan-reap.d.ts +0 -253
- package/dist/lib/tmux/orphan-reap.js +3 -299
- package/dist/lib/tmux/paths.d.ts +0 -14
- package/dist/lib/tmux/paths.js +0 -14
- package/dist/lib/tmux/session.d.ts +0 -263
- package/dist/lib/tmux/session.js +6 -344
- package/dist/lib/traces/backend.d.ts +0 -20
- package/dist/lib/traces/backend.js +0 -23
- package/dist/lib/traces/classify.d.ts +0 -20
- package/dist/lib/traces/classify.js +0 -7
- package/dist/lib/traces/insights.d.ts +1 -84
- package/dist/lib/traces/insights.js +0 -126
- package/dist/lib/traces/phenotype.d.ts +0 -69
- package/dist/lib/traces/phenotype.js +0 -150
- package/dist/lib/traces/provision.d.ts +0 -1
- package/dist/lib/traces/provision.js +0 -5
- package/dist/lib/traces/schema2-build.d.ts +3 -60
- package/dist/lib/traces/schema2-build.js +8 -149
- package/dist/lib/traces/schema2-danger.d.ts +0 -30
- package/dist/lib/traces/schema2-danger.js +1 -63
- package/dist/lib/traces/schema2.d.ts +0 -11
- package/dist/lib/traces/segments.d.ts +0 -44
- package/dist/lib/traces/segments.js +0 -65
- package/dist/lib/traces/sync.d.ts +2 -239
- package/dist/lib/traces/sync.js +15 -237
- package/dist/lib/traces/worker-template.d.ts +0 -3
- package/dist/lib/traces/worker-template.js +0 -29
- package/dist/lib/triggers/handlers.d.ts +0 -82
- package/dist/lib/triggers/handlers.js +0 -75
- package/dist/lib/triggers/webhook.d.ts +0 -158
- package/dist/lib/triggers/webhook.js +0 -143
- package/dist/lib/types.d.ts +0 -736
- package/dist/lib/types.js +0 -50
- package/dist/lib/uninstall.d.ts +0 -17
- package/dist/lib/uninstall.js +0 -75
- package/dist/lib/usage-backoff.d.ts +0 -20
- package/dist/lib/usage-backoff.js +3 -60
- package/dist/lib/usage-refresh.d.ts +0 -154
- package/dist/lib/usage-refresh.js +8 -212
- package/dist/lib/verbs.d.ts +0 -12
- package/dist/lib/verbs.js +0 -12
- package/dist/lib/version-duplicates.d.ts +0 -6
- package/dist/lib/version-duplicates.js +0 -6
- package/dist/lib/version.d.ts +0 -55
- package/dist/lib/version.js +1 -58
- package/dist/lib/view-types.d.ts +0 -48
- package/dist/lib/warn-unpushed.d.ts +0 -27
- package/dist/lib/warn-unpushed.js +1 -45
- package/dist/lib/watchdog/history.d.ts +0 -1
- package/dist/lib/watchdog/history.js +0 -3
- package/dist/lib/watchdog/index.js +0 -2
- package/dist/lib/watchdog/log.d.ts +0 -19
- package/dist/lib/watchdog/log.js +0 -34
- package/dist/lib/watchdog/read.d.ts +0 -27
- package/dist/lib/watchdog/read.js +3 -58
- package/dist/lib/watchdog/rotate.d.ts +0 -139
- package/dist/lib/watchdog/rotate.js +2 -151
- package/dist/lib/watchdog/runner.d.ts +0 -125
- package/dist/lib/watchdog/runner.js +3 -223
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -2
- package/dist/lib/watchdog/watchdog-agent.d.ts +0 -31
- package/dist/lib/watchdog/watchdog-agent.js +0 -27
- package/dist/lib/watchdog/watchdog.d.ts +0 -7
- package/dist/lib/watchdog/watchdog.js +0 -10
- package/dist/lib/watchdog/watchdogTail.js +0 -7
- package/dist/lib/whats-new.d.ts +0 -10
- package/dist/lib/whats-new.js +0 -21
- package/dist/lib/workflows-registry.d.ts +0 -45
- package/dist/lib/workflows-registry.js +0 -60
- package/dist/lib/workflows.d.ts +0 -282
- package/dist/lib/workflows.js +3 -213
- package/dist/lib/worktree/held.d.ts +0 -89
- package/dist/lib/worktree/held.js +1 -103
- package/dist/lib/wrap.d.ts +0 -32
- package/dist/lib/wrap.js +0 -32
- package/dist/lib/yaml-io.d.ts +0 -45
- package/dist/lib/yaml-io.js +0 -47
- package/dist/session-tracker/dist/adapters/claude.js +0 -2
- package/dist/session-tracker/dist/hook.sh +4 -37
- package/dist/session-tracker/dist/index.d.ts +0 -7
- package/dist/session-tracker/dist/index.js +0 -5
- package/dist/session-tracker/dist/install-hook.d.ts +0 -5
- package/dist/session-tracker/dist/install-hook.js +1 -15
- package/dist/session-tracker/dist/prune-state.js +0 -4
- package/dist/session-tracker/dist/reader.js +0 -4
- package/dist/session-tracker/dist/state-file.d.ts +0 -3
- package/dist/session-tracker/dist/state-file.js +3 -8
- package/package.json +3 -4
- package/scripts/postinstall.js +8 -116
- package/dist/lib/session/artifacts.d.ts +0 -14
- package/dist/lib/session/artifacts.js +0 -101
- package/dist/lib/session/bash-command.d.ts +0 -63
- package/dist/lib/session/bash-command.js +0 -490
- package/dist/lib/session/digest.d.ts +0 -66
- package/dist/lib/session/digest.js +0 -328
- package/dist/lib/session/highlights.d.ts +0 -151
- package/dist/lib/session/highlights.js +0 -391
- package/dist/lib/session/insights.d.ts +0 -165
- package/dist/lib/session/insights.js +0 -563
- package/dist/lib/session/linear.d.ts +0 -19
- package/dist/lib/session/linear.js +0 -99
- package/dist/lib/session/parse.d.ts +0 -224
- package/dist/lib/session/parse.js +0 -2408
- package/dist/lib/session/prompt.d.ts +0 -213
- package/dist/lib/session/prompt.js +0 -601
- package/dist/lib/session/render.d.ts +0 -133
- package/dist/lib/session/render.js +0 -1047
- package/dist/lib/session/share-html.d.ts +0 -35
- package/dist/lib/session/share-html.js +0 -319
- package/dist/lib/session/shell-programs.d.ts +0 -33
- package/dist/lib/session/shell-programs.js +0 -380
- package/dist/lib/session/state.d.ts +0 -269
- package/dist/lib/session/state.js +0 -777
- package/dist/lib/session/stream-render.d.ts +0 -4
- package/dist/lib/session/stream-render.js +0 -131
- package/dist/lib/session/tail.d.ts +0 -88
- package/dist/lib/session/tail.js +0 -294
- package/dist/lib/session/team-filter.d.ts +0 -129
- package/dist/lib/session/team-filter.js +0 -269
- package/dist/lib/session/timeline.d.ts +0 -172
- package/dist/lib/session/timeline.js +0 -636
- package/dist/lib/session/tool-calls.d.ts +0 -132
- package/dist/lib/session/tool-calls.js +0 -674
- package/dist/lib/session/trajectory-compare.d.ts +0 -48
- package/dist/lib/session/trajectory-compare.js +0 -116
- package/dist/lib/session/trajectory-html.d.ts +0 -19
- package/dist/lib/session/trajectory-html.js +0 -771
- package/dist/lib/session/trajectory-lineage.d.ts +0 -93
- package/dist/lib/session/trajectory-lineage.js +0 -195
- package/dist/lib/session/trajectory-text.d.ts +0 -38
- package/dist/lib/session/trajectory-text.js +0 -284
- package/dist/lib/session/trajectory.d.ts +0 -145
- package/dist/lib/session/trajectory.js +0 -394
- package/dist/lib/session/types.d.ts +0 -627
- package/dist/lib/session/types.js +0 -43
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session discovery, search, and rendering commands.
|
|
3
|
-
*
|
|
4
|
-
* Implements `agents sessions` -- the unified interface for finding, browsing,
|
|
5
|
-
* and reading agent conversation transcripts across Claude, Codex, Gemini,
|
|
6
|
-
* and OpenCode. Supports interactive picker mode, text/path search, markdown
|
|
7
|
-
* and JSON rendering, role/turn filtering, artifact inspection, and session
|
|
8
|
-
* resume via agent-native CLI flags.
|
|
9
|
-
*/
|
|
10
1
|
import * as fs from 'fs';
|
|
11
2
|
import * as os from 'os';
|
|
12
3
|
import * as path from 'path';
|
|
@@ -19,33 +10,31 @@ import { resolveProjectKey } from '../lib/project-key.js';
|
|
|
19
10
|
import { listProjectDefs, resolveProjectNameForCwd } from '../lib/projects.js';
|
|
20
11
|
import ora from 'ora';
|
|
21
12
|
import { interruptibleSpinner } from '../lib/spinner.js';
|
|
22
|
-
import { SESSION_AGENTS, isAgentTmuxAlias, sessionDisplayAgent } from '
|
|
23
|
-
import { discoverArtifacts, readArtifact, resolveArtifact } from '
|
|
13
|
+
import { SESSION_AGENTS, isAgentTmuxAlias, sessionDisplayAgent } from '@phnx-labs/sessions-cli/reader';
|
|
14
|
+
import { discoverArtifacts, readArtifact, resolveArtifact } from '@phnx-labs/sessions-cli/reader';
|
|
24
15
|
import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
25
|
-
import { getActiveSessions,
|
|
16
|
+
import { getActiveSessions, serializeSessionsJson, shortIdFromName } from '../lib/session/active.js';
|
|
26
17
|
export { activeSessionProjectKey, backfillActiveRowsFromIndex, backfillActiveRowsFromMeta, isRunningLiveSession, serializeActiveSessionsForJson, serializeSessionsJson } from '../lib/session/active.js';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import { gatherRemoteActive, NO_FANOUT_ENV } from '../lib/session/remote-active.js';
|
|
32
|
-
import { loadFleetActiveSessions, loadLocalActiveSessions, readActiveSessionsCache, } from '../lib/session/session-cache.js';
|
|
18
|
+
import { viewingInLabel } from '../lib/session/viewing-in.js';
|
|
19
|
+
import { machineId } from '../lib/session/sync/config.js';
|
|
20
|
+
import { NO_FANOUT_ENV } from '../lib/session/remote-active.js';
|
|
21
|
+
import { loadLocalActiveSessions, readActiveSessionsCache, } from '../lib/session/session-cache.js';
|
|
33
22
|
import { gatherRemoteList, gatherRemoteToolProgramCounts, gatherRemoteToolSearch, runOnPeer } from '../lib/session/remote-list.js';
|
|
34
23
|
import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
35
24
|
import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/session/width.js';
|
|
36
|
-
import { inferSessionState } from '
|
|
25
|
+
import { inferSessionState } from '@phnx-labs/sessions-cli/reader';
|
|
37
26
|
import { discoverSessions, queryIndexedSessions, countSessionsInScope, resolveSessionById, isCompleteSessionId, looksLikeSessionId, searchContentIndex, getSessionRoots, scopeToManaged } from '../lib/session/discover.js';
|
|
38
27
|
import { findSessionsById, querySessions, readSessionContent, readArchivedSessionPreview, readSessionTimelineAny } from '../lib/session/db.js';
|
|
39
|
-
import { foldTimeline, emptyTimelineState, projectTimeline, projectSessionFiles } from '
|
|
40
|
-
import { readSessionTail } from '
|
|
28
|
+
import { foldTimeline, emptyTimelineState, projectTimeline, projectSessionFiles } from '@phnx-labs/sessions-cli/reader';
|
|
29
|
+
import { readSessionTail } from '@phnx-labs/sessions-cli/reader';
|
|
41
30
|
import { liveSessionMetas, fleetExecutionMachineById, reconcileLiveMetaMachine } from '../lib/session/live-metadata.js';
|
|
42
31
|
import { sessionHeadline } from '../lib/session/title.js';
|
|
43
|
-
import { filterTeamSessions, shouldShowTeamSessions, safeTeamText, groupSessionsByTeam, NO_TEAM_GROUP_KEY, } from '
|
|
44
|
-
import { parseSession } from '
|
|
32
|
+
import { filterTeamSessions, shouldShowTeamSessions, safeTeamText, groupSessionsByTeam, NO_TEAM_GROUP_KEY, } from '@phnx-labs/sessions-cli/reader';
|
|
33
|
+
import { parseSession } from '@phnx-labs/sessions-cli/reader';
|
|
45
34
|
import { runRemoteSessions, buildForwardedArgs, ensureWholeIndex } from '../lib/session/remote.js';
|
|
46
|
-
import { formatRelativeTime,
|
|
47
|
-
import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList, linkPath, linkUrl, shortenModel, formatTokenCount } from '
|
|
48
|
-
import { linearIssueUrl } from '
|
|
35
|
+
import { formatRelativeTime, sessionAgeParts } from '../lib/session/relative-time.js';
|
|
36
|
+
import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList, linkPath, linkUrl, shortenModel, formatTokenCount } from '@phnx-labs/sessions-cli/reader';
|
|
37
|
+
import { linearIssueUrl } from '@phnx-labs/sessions-cli/reader';
|
|
49
38
|
import { sessionOwnerDevice, RESUME_PINNED_ENV } from '../lib/session/resume-owner.js';
|
|
50
39
|
import { renderMarkdown } from '../lib/markdown.js';
|
|
51
40
|
import { AGENTS, colorAgent, resolveAgentName } from '../lib/agents.js';
|
|
@@ -59,8 +48,9 @@ import { listInstalledVersions, resolveVersionAliasLoose } from '../lib/installa
|
|
|
59
48
|
import { getAgentsInvocation } from '../lib/daemon/daemon.js';
|
|
60
49
|
import { sessionAgentSupportsResume, sessionRecoveryRunArgs } from '../lib/session/recovery.js';
|
|
61
50
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
62
|
-
import { sessionPicker, buildPreview, loadSessionPreviewDigest, transcriptOnPeerOf, formatTodoCompact,
|
|
51
|
+
import { sessionPicker, buildPreview, loadSessionPreviewDigest, transcriptOnPeerOf, formatTodoCompact, } from './sessions-picker.js';
|
|
63
52
|
import { setHelpSections } from '../lib/help.js';
|
|
53
|
+
import { cleanPreview, gatherActiveSessions, indexActiveBySessionId, isAwaitingUser, liveGlyphAndPreview, liveStatusWord, remoteHostsToDial, requestedLiveStatuses, resolveRoutineName, runLiveRoster, shouldIncludeLocal, signalBadges, statusColor, } from './ps-roster.js';
|
|
64
54
|
import { registerSessionsTailCommand } from './sessions-tail.js';
|
|
65
55
|
import { registerSessionsResumeCommand } from './sessions-resume.js';
|
|
66
56
|
import { registerSessionsForkCommand } from './fork.js';
|
|
@@ -90,17 +80,7 @@ const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2
|
|
|
90
80
|
function collectQueryClause(value, previous) {
|
|
91
81
|
return [...previous, value];
|
|
92
82
|
}
|
|
93
|
-
/**
|
|
94
|
-
* The prioritized harnesses that get a boolean shorthand flag (e.g. `--claude`
|
|
95
|
-
* === `--agent claude`). The rest stay reachable via `--agent <name>`, which
|
|
96
|
-
* also carries version pins like `codex@0.116.0`.
|
|
97
|
-
*/
|
|
98
83
|
const AGENT_SHORTHANDS = ['claude', 'codex', 'kimi', 'antigravity', 'grok', 'opencode'];
|
|
99
|
-
/**
|
|
100
|
-
* Resolve a per-agent shorthand (`--claude`, `--kimi`, …) into `options.agent`.
|
|
101
|
-
* An explicit `--agent` wins; if two shorthands are passed we take the first and
|
|
102
|
-
* ignore the rest (commander gives no ordering, so this is a best-effort alias).
|
|
103
|
-
*/
|
|
104
84
|
function applyAgentShorthands(options) {
|
|
105
85
|
if (options.agent)
|
|
106
86
|
return;
|
|
@@ -108,10 +88,6 @@ function applyAgentShorthands(options) {
|
|
|
108
88
|
if (hit)
|
|
109
89
|
options.agent = hit;
|
|
110
90
|
}
|
|
111
|
-
/**
|
|
112
|
-
* Treat a positional `agent@version` as a structured filter only when it names
|
|
113
|
-
* a real installed version. Everything else remains ordinary free-text search.
|
|
114
|
-
*/
|
|
115
91
|
export function parseInstalledAgentVersionQuery(query, installedVersions = (agent) => (agent in AGENTS ? listInstalledVersions(agent) : [])) {
|
|
116
92
|
const trimmed = query?.trim();
|
|
117
93
|
if (!trimmed)
|
|
@@ -149,7 +125,6 @@ function applyVersionFilters(query, options) {
|
|
|
149
125
|
const CLAUDE_RESUME_MATCH_WINDOW_MS = 10 * 60_000;
|
|
150
126
|
const LOAD_VERBS = ['Loading', 'Scanning', 'Gathering', 'Indexing', 'Reading'];
|
|
151
127
|
const FIND_VERBS = ['Finding', 'Searching', 'Locating', 'Matching'];
|
|
152
|
-
/** Build a spinner-backed progress tracker that cycles through verbs while scanning sessions. */
|
|
153
128
|
function createScanProgressTracker(verbs, suffix, spinner) {
|
|
154
129
|
const counts = new Map();
|
|
155
130
|
let verbIndex = 0;
|
|
@@ -186,24 +161,11 @@ function createScanProgressTracker(verbs, suffix, spinner) {
|
|
|
186
161
|
};
|
|
187
162
|
}
|
|
188
163
|
const PICKER_RECENT_COUNT = 15;
|
|
189
|
-
/**
|
|
190
|
-
* The `--limit` default, shared with its `.option()` registration. Commander fills
|
|
191
|
-
* the default in, so `options.limit` is never falsy — code that wants to know
|
|
192
|
-
* whether the USER set a limit has to compare against this rather than test
|
|
193
|
-
* truthiness.
|
|
194
|
-
*/
|
|
195
164
|
const DEFAULT_LIMIT = '50';
|
|
196
|
-
/** Pool size for `--in-team`: one team's rows can sit anywhere in the history. */
|
|
197
165
|
const WHOLE_TEAM_POOL_LIMIT = 5000;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const OVERVIEW_ROWS_PER_PROJECT = 5; // recent rows shown per project before "· N more"
|
|
202
|
-
const OVERVIEW_POOL_LIMIT = 1000; // fetch cap — accurate per-project totals up to this
|
|
203
|
-
const OVERVIEW_MAX_PROJECTS = 12; // project groups shown before "+N more projects"
|
|
204
|
-
/**
|
|
205
|
-
* Resolve a path-like query to an absolute directory path.
|
|
206
|
-
*/
|
|
166
|
+
const OVERVIEW_ROWS_PER_PROJECT = 5;
|
|
167
|
+
const OVERVIEW_POOL_LIMIT = 1000;
|
|
168
|
+
const OVERVIEW_MAX_PROJECTS = 12;
|
|
207
169
|
function resolvePathFilter(query) {
|
|
208
170
|
const expanded = query.startsWith('~')
|
|
209
171
|
? path.join(os.homedir(), query.slice(1))
|
|
@@ -251,28 +213,6 @@ async function renderArtifactsForSession(session, listAll, name) {
|
|
|
251
213
|
}
|
|
252
214
|
console.log(chalk.gray(`\n${artifacts.length} artifact${artifacts.length !== 1 ? 's' : ''}.`));
|
|
253
215
|
}
|
|
254
|
-
function statusColor(status) {
|
|
255
|
-
switch (status) {
|
|
256
|
-
case 'running': return chalk.green;
|
|
257
|
-
case 'idle': return chalk.gray;
|
|
258
|
-
case 'queued': return chalk.blue;
|
|
259
|
-
case 'input_required': return chalk.yellow;
|
|
260
|
-
// Dead process: dimmed so it recedes from the live rows without being mistaken
|
|
261
|
-
// for the gray `idle` (which reads as "done, waiting for you" — a live state).
|
|
262
|
-
case 'closed': return chalk.dim;
|
|
263
|
-
// Days-stale / dangling: red so a session nobody is driving stands out.
|
|
264
|
-
case 'abandoned': return chalk.red;
|
|
265
|
-
// The host window died and took the agent with it — an unclean exit, not the
|
|
266
|
-
// dimmed `closed` of a normal one. Red-bright so a crash reads as an event.
|
|
267
|
-
case 'crashed': return chalk.redBright;
|
|
268
|
-
// Alive with nobody attached. Yellow, like `input_required`: both mean the
|
|
269
|
-
// session is stuck waiting on a human, and this one has no human to wait for.
|
|
270
|
-
case 'orphaned': return chalk.yellow;
|
|
271
|
-
// Alive but un-introspectable (a harness whose transcript we can't parse).
|
|
272
|
-
// Magenta so it never reads as the gray "idle" it used to be faked as.
|
|
273
|
-
case 'unknown': return chalk.magenta;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
216
|
function contextColor(context) {
|
|
277
217
|
switch (context) {
|
|
278
218
|
case 'terminal': return chalk.magenta;
|
|
@@ -285,9 +225,6 @@ function shortCwd(cwd) {
|
|
|
285
225
|
if (!cwd)
|
|
286
226
|
return '-';
|
|
287
227
|
const home = homeDir();
|
|
288
|
-
// Compare in normalized form so the `~` shorthand also lands on Windows
|
|
289
|
-
// (case-insensitive, backslash paths); on POSIX this is byte-identical to the
|
|
290
|
-
// previous `cwd.startsWith(home)`. The displayed tail keeps original casing.
|
|
291
228
|
return toComparablePath(cwd).startsWith(toComparablePath(home))
|
|
292
229
|
? '~' + cwd.slice(home.length)
|
|
293
230
|
: cwd;
|
|
@@ -297,276 +234,13 @@ function formatStartedAt(startedAtMs) {
|
|
|
297
234
|
return '-';
|
|
298
235
|
return formatRelativeTime(new Date(startedAtMs).toISOString());
|
|
299
236
|
}
|
|
300
|
-
/**
|
|
301
|
-
* Strip terminal/harness noise from a preview so the column stays a single line
|
|
302
|
-
* of plain prose: OSC title escapes, CSI/SGR ANSI, and the harness wrapper tags
|
|
303
|
-
* (`<local-command-stdout>`, `<task-notification>`, `<command-*>`) that leak from
|
|
304
|
-
* a captured transcript tail. Collapses runs of whitespace.
|
|
305
|
-
*/
|
|
306
|
-
export function cleanPreview(text) {
|
|
307
|
-
// eslint-disable-next-line no-control-regex
|
|
308
|
-
return text
|
|
309
|
-
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '') // OSC (title) sequences
|
|
310
|
-
.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '') // CSI / SGR ANSI
|
|
311
|
-
.replace(/<\/?(?:local-command-stdout|command-name|command-message|command-args|task-notification|system-reminder)>/g, '')
|
|
312
|
-
.replace(/\s+/g, ' ')
|
|
313
|
-
.trim();
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Build the live description for an active session: checklist progress (when
|
|
317
|
-
* present) plus the state engine's preview (the latest turn), a user label, or
|
|
318
|
-
* the first-prompt topic. Used by both the flat listing's `doing` cell and as
|
|
319
|
-
* the snippet half of the --active row (identity is layered on in printActiveRow).
|
|
320
|
-
*
|
|
321
|
-
* Covers every ActiveSession context: terminal (interactive), headless, teams,
|
|
322
|
-
* cloud, and sub-agent rows that share the same ActiveSession.todos field.
|
|
323
|
-
*/
|
|
324
|
-
export function buildSessionDescription(s) {
|
|
325
|
-
const todo = formatTodoCompact(s.todos);
|
|
326
|
-
if (s.context === 'cloud') {
|
|
327
|
-
const base = s.preview || `${s.cloudProvider ?? ''}${s.cloudTaskId ? ` · ${s.cloudTaskId.slice(0, 12)}` : ''}`;
|
|
328
|
-
return cleanPreview([todo, base].filter(Boolean).join(' · '));
|
|
329
|
-
}
|
|
330
|
-
if (s.context === 'teams') {
|
|
331
|
-
// A teams row identifies its TEAM, then the teammate within it, then who
|
|
332
|
-
// spun it up, then what it's working on — so several teams from one
|
|
333
|
-
// orchestrator stay distinct and each shows its target, not just its slug.
|
|
334
|
-
const parts = [s.teamName];
|
|
335
|
-
// Teammate name (distinct from the team slug) — which member this row is.
|
|
336
|
-
if (s.label && s.label !== s.teamName)
|
|
337
|
-
parts.push(s.label);
|
|
338
|
-
// Lineage: which orchestrator spun up this team. Prefer the resolved label,
|
|
339
|
-
// else the short session id, so "by whom" is answerable at a glance.
|
|
340
|
-
const orch = s.orchestratorLabel || (s.orchestratorSessionId ? s.orchestratorSessionId.slice(0, 8) : '');
|
|
341
|
-
if (orch)
|
|
342
|
-
parts.push(`by ${orch}`);
|
|
343
|
-
if (todo)
|
|
344
|
-
parts.push(todo);
|
|
345
|
-
// Target: the live latest turn if working, else the assigned mission (the
|
|
346
|
-
// team's task/target, shown even before the teammate has a transcript), else
|
|
347
|
-
// the transcript topic.
|
|
348
|
-
const target = s.preview || s.assignedTask || s.topic;
|
|
349
|
-
if (target)
|
|
350
|
-
parts.push(target);
|
|
351
|
-
return cleanPreview(parts.filter(Boolean).join(' · '));
|
|
352
|
-
}
|
|
353
|
-
// Terminal, headless, or sub-agent: todos + live preview, then label, then topic.
|
|
354
|
-
// ladder-exempt: the compact --active preview BASE (a live snippet), not the row's headline title.
|
|
355
|
-
const base = s.preview || s.label || s.topic || '';
|
|
356
|
-
return cleanPreview([todo, base].filter(Boolean).join(' · '));
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* Identity + checklist + live snippet for an --active / cross-machine row.
|
|
360
|
-
* Surfaces agent-adjacent identity the flat table already has (label, project)
|
|
361
|
-
* with a clickable project when a GitHub URL is resolvable, then the checklist
|
|
362
|
-
* tally and the latest-turn snippet.
|
|
363
|
-
*
|
|
364
|
-
* Free-text fields are cleaned individually; OSC 8 hyperlinks are applied
|
|
365
|
-
* *after* cleaning so `cleanPreview` does not strip the clickable targets
|
|
366
|
-
* (RUSH-2045 review). Ticket is clickable via {@link signalBadges}, not here,
|
|
367
|
-
* so the id is not printed twice.
|
|
368
|
-
*/
|
|
369
|
-
export function formatActiveRowDescription(s) {
|
|
370
|
-
const parts = [];
|
|
371
|
-
const pushText = (t) => {
|
|
372
|
-
const c = t ? cleanPreview(t) : '';
|
|
373
|
-
if (c)
|
|
374
|
-
parts.push(c);
|
|
375
|
-
};
|
|
376
|
-
if (s.context === 'teams' && s.teamName)
|
|
377
|
-
pushText(s.teamName);
|
|
378
|
-
if (s.label)
|
|
379
|
-
pushText(s.label);
|
|
380
|
-
// Project = basename(cwd), same derivation as serializeActiveSessionsForJson.
|
|
381
|
-
const project = s.cwd ? path.basename(s.cwd) : '';
|
|
382
|
-
if (project && project !== s.label && project !== s.teamName) {
|
|
383
|
-
const label = cleanPreview(project);
|
|
384
|
-
const repoUrl = githubRepoUrlFromCwd(s.cwd);
|
|
385
|
-
parts.push(repoUrl ? linkUrl(repoUrl, label) : label);
|
|
386
|
-
}
|
|
387
|
-
const todo = formatTodoCompact(s.todos);
|
|
388
|
-
if (todo)
|
|
389
|
-
parts.push(todo);
|
|
390
|
-
// Latest-turn snippet — avoid repeating label/topic when already used as identity.
|
|
391
|
-
// A cloud row with no preview yet says nothing here; its provider + task id
|
|
392
|
-
// ride the locator badge instead (RUSH-2336), not duplicated on this line.
|
|
393
|
-
if (s.preview) {
|
|
394
|
-
pushText(s.preview);
|
|
395
|
-
}
|
|
396
|
-
else if (!s.label && s.topic) {
|
|
397
|
-
pushText(s.topic);
|
|
398
|
-
}
|
|
399
|
-
return parts.filter(Boolean).join(' · ');
|
|
400
|
-
}
|
|
401
|
-
/** Short human word for a session's activity (falls back to the coarse status). */
|
|
402
|
-
function activityLabel(s) {
|
|
403
|
-
// Lifecycle status wins over any residual parsed activity — a dead/dangling
|
|
404
|
-
// session must read `closed`/`abandoned`, not the `idle` its stale tail infers.
|
|
405
|
-
// `crashed`/`orphaned` are the same kind of claim about the session as a whole,
|
|
406
|
-
// and both outrank the `idle` its last parsed turn would otherwise show.
|
|
407
|
-
if (s.status === 'closed' || s.status === 'abandoned')
|
|
408
|
-
return s.status;
|
|
409
|
-
if (s.status === 'crashed')
|
|
410
|
-
return 'crashed';
|
|
411
|
-
if (s.status === 'orphaned')
|
|
412
|
-
return 'orphan';
|
|
413
|
-
if (s.activity === 'waiting_input')
|
|
414
|
-
return 'waiting';
|
|
415
|
-
if (s.activity === 'working')
|
|
416
|
-
return 'working';
|
|
417
|
-
if (s.activity === 'idle')
|
|
418
|
-
return 'idle';
|
|
419
|
-
return s.status === 'input_required' ? 'waiting' : s.status;
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Index live sessions by their full session UUID so a historical `SessionMeta`
|
|
423
|
-
* row (`meta.id`) can be matched to the session that is still running now.
|
|
424
|
-
* Rows without a sessionId (some cloud/headless probes) are skipped — they
|
|
425
|
-
* can't be correlated back to a transcript on disk.
|
|
426
|
-
*/
|
|
427
|
-
export function indexActiveBySessionId(active) {
|
|
428
|
-
const byId = new Map();
|
|
429
|
-
for (const a of active) {
|
|
430
|
-
if (a.sessionId)
|
|
431
|
-
byId.set(a.sessionId, a);
|
|
432
|
-
}
|
|
433
|
-
return byId;
|
|
434
|
-
}
|
|
435
|
-
/**
|
|
436
|
-
* The live decoration for a listing row: a status glyph and the latest-turn
|
|
437
|
-
* preview, when the session is still running. `●` running / `◐` waiting on the
|
|
438
|
-
* user / `○` idle, colored by the same `statusColor` the --active view uses.
|
|
439
|
-
* Returns empty strings when there is no live match, so callers render the
|
|
440
|
-
* plain historical row unchanged.
|
|
441
|
-
*/
|
|
442
|
-
export function liveGlyphAndPreview(a) {
|
|
443
|
-
if (!a)
|
|
444
|
-
return { glyph: '', preview: '' };
|
|
445
|
-
// `◌` (dotted) = alive but un-introspectable — visually distinct from `○` idle
|
|
446
|
-
// so an opaque harness is never mistaken for a finished one. `⊘` = abandoned /
|
|
447
|
-
// dangling; `×` = closed (dead pid) — both distinct from the `○` idle a live,
|
|
448
|
-
// stopped session shows, so a gone session never masquerades as a resting one.
|
|
449
|
-
// `✗` = crashed (died WITH its host window, uncleanly) — deliberately louder
|
|
450
|
-
// than the `×` of a clean close. `◍` = orphaned (alive, nothing attached): a
|
|
451
|
-
// filled ring, so it reads as "still burning" unlike the hollow `○` idle.
|
|
452
|
-
if (a.status === 'abandoned')
|
|
453
|
-
return { glyph: statusColor(a.status)('⊘'), preview: buildSessionDescription(a) };
|
|
454
|
-
if (a.status === 'closed')
|
|
455
|
-
return { glyph: statusColor(a.status)('×'), preview: buildSessionDescription(a) };
|
|
456
|
-
if (a.status === 'crashed')
|
|
457
|
-
return { glyph: statusColor(a.status)('✗'), preview: buildSessionDescription(a) };
|
|
458
|
-
if (a.status === 'orphaned')
|
|
459
|
-
return { glyph: statusColor(a.status)('◍'), preview: buildSessionDescription(a) };
|
|
460
|
-
const waiting = a.status === 'input_required' || a.activity === 'waiting_input';
|
|
461
|
-
const running = a.status === 'running' || a.activity === 'working';
|
|
462
|
-
const unknown = a.status === 'unknown';
|
|
463
|
-
const shape = waiting ? '◐'
|
|
464
|
-
: running ? '●'
|
|
465
|
-
: unknown ? '◌'
|
|
466
|
-
: '○';
|
|
467
|
-
return { glyph: statusColor(a.status)(shape), preview: buildSessionDescription(a) };
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* The one-word live status for a listing row — `working` / `waiting` / `idle`,
|
|
471
|
-
* the same three states the `--active` column shows, so the default list is no
|
|
472
|
-
* longer just a glyph. `waiting` is the actionable "needs you" case (a question /
|
|
473
|
-
* permission / plan-review), kept distinct from `idle` (stopped) and `working`.
|
|
474
|
-
* Empty for a not-live row, and for the rare no-signal `unknown`. Pure +
|
|
475
|
-
* exported for the row tests.
|
|
476
|
-
*/
|
|
477
|
-
export function liveStatusWord(a) {
|
|
478
|
-
if (!a)
|
|
479
|
-
return '';
|
|
480
|
-
// Lifecycle status is definitive — surface it ahead of any parsed activity.
|
|
481
|
-
if (a.status === 'closed' || a.status === 'abandoned')
|
|
482
|
-
return a.status;
|
|
483
|
-
// Same rank: a lost host is a fact about the session, not about its last turn.
|
|
484
|
-
if (a.status === 'crashed')
|
|
485
|
-
return 'crashed';
|
|
486
|
-
if (a.status === 'orphaned')
|
|
487
|
-
return 'orphan';
|
|
488
|
-
if (a.status === 'input_required' || a.activity === 'waiting_input')
|
|
489
|
-
return 'waiting';
|
|
490
|
-
if (a.status === 'running' || a.activity === 'working')
|
|
491
|
-
return 'working';
|
|
492
|
-
if (a.status === 'idle' || a.activity === 'idle')
|
|
493
|
-
return 'idle';
|
|
494
|
-
if (a.status === 'queued')
|
|
495
|
-
return 'queued';
|
|
496
|
-
return '';
|
|
497
|
-
}
|
|
498
|
-
/**
|
|
499
|
-
* True when a session is blocked on a human — the `--waiting` contract.
|
|
500
|
-
*
|
|
501
|
-
* NOT `status === 'input_required'`. `foldHostLink` rewrites that status to
|
|
502
|
-
* `orphaned` when nothing is attached, and a session waiting on a question with
|
|
503
|
-
* NOBODY watching is the most acute case `--waiting` exists to surface, not one
|
|
504
|
-
* it should drop. The underlying `activity` is never rewritten, so it is the
|
|
505
|
-
* honest signal here.
|
|
506
|
-
*
|
|
507
|
-
* But `activity` is never rewritten for a DEAD session either: one that died
|
|
508
|
-
* mid-question keeps `waiting_input` forever, and answering it is not a thing a
|
|
509
|
-
* human can do — it needs a relaunch. `--waiting` is a scriptable gate ("does
|
|
510
|
-
* anything need me?"), so a corpse must not trip it.
|
|
511
|
-
*
|
|
512
|
-
* `closed` and `crashed` are unconditionally dead, so they are excluded outright.
|
|
513
|
-
* `abandoned` is NOT: it fires on transcript staleness before the liveness check,
|
|
514
|
-
* so it also covers the live-but-forgotten case — an interactive session that
|
|
515
|
-
* asked a question and sat untouched over a long weekend is still answerable, and
|
|
516
|
-
* is exactly what this gate exists for. It is excluded only when we positively
|
|
517
|
-
* know its process is gone; unknown liveness (an older peer, a row with no pid)
|
|
518
|
-
* stays excluded rather than inventing a human who can answer.
|
|
519
|
-
*/
|
|
520
|
-
export function isAwaitingUser(s) {
|
|
521
|
-
if (s.status === 'crashed' || s.status === 'closed')
|
|
522
|
-
return false;
|
|
523
|
-
if (s.status === 'abandoned' && s.pidAlive !== true)
|
|
524
|
-
return false;
|
|
525
|
-
return s.status === 'input_required' || s.activity === 'waiting_input';
|
|
526
|
-
}
|
|
527
|
-
/**
|
|
528
|
-
* Whether a row belongs in an unqualified `--active` view (RUSH-2336) — the
|
|
529
|
-
* ONE canonical selector every bare-active surface shares: the CLI's grouped
|
|
530
|
-
* table and `--json` (`renderActiveSessions`), the interactive browser's
|
|
531
|
-
* `--active` filter ({@link applyFilters} in `sessions-browser.ts`), `focus`'s
|
|
532
|
-
* attach gate (`isAttachableLiveSession`), and the menubar snapshot
|
|
533
|
-
* ({@link computeMenubarSnapshot}).
|
|
534
|
-
*
|
|
535
|
-
* The live registry deliberately retains terminally-dead AND not-yet-started
|
|
536
|
-
* rows long enough for `--closed` / `--crashed` / `--queued` and recovery to
|
|
537
|
-
* find them. Presence in that registry therefore is not, by itself, evidence
|
|
538
|
-
* that a session is CURRENTLY active. Explicit lifecycle filters bypass this
|
|
539
|
-
* predicate and select those retained rows through {@link matchesLiveStatus}
|
|
540
|
-
* instead — this function only decides the unqualified default.
|
|
541
|
-
*
|
|
542
|
-
* - `queued` / `closed` / `crashed` are never active here: queued hasn't
|
|
543
|
-
* started yet, closed/crashed are unconditionally dead. All three stay
|
|
544
|
-
* reachable through their explicit filter.
|
|
545
|
-
* - A cloud row carries no local pid at all — it is active on the
|
|
546
|
-
* provider's own word (`cloudProvider` + `cloudTaskId`), never a
|
|
547
|
-
* fabricated pid.
|
|
548
|
-
* - Every other row is a real OS process (terminal/tmux/headless/team). It
|
|
549
|
-
* is active only when the row names the machine it runs on, carries a
|
|
550
|
-
* positive pid, AND has POSITIVELY verified that pid is alive
|
|
551
|
-
* (`pidAlive === true` — not merely "not known dead"). A live orphaned
|
|
552
|
-
* row, or a live-but-stuck abandoned row, still qualifies this way since
|
|
553
|
-
* both carry a genuinely alive pid; unknown liveness (an older peer's
|
|
554
|
-
* row, or a pid that could never be resolved) does not.
|
|
555
|
-
*/
|
|
556
|
-
/** Width of the live status column — `crashed` is the longest word it renders. */
|
|
557
237
|
const LIVE_STATUS_W = 8;
|
|
558
|
-
/** The colored, space-padded status cell for a listing row (empty when not live). */
|
|
559
238
|
function liveStatusCell(live) {
|
|
560
239
|
const word = liveStatusWord(live);
|
|
561
240
|
if (!word || !live)
|
|
562
241
|
return { cell: '', width: 0 };
|
|
563
242
|
return { cell: statusColor(live.status)(padToWidth(word, LIVE_STATUS_W)), width: LIVE_STATUS_W };
|
|
564
243
|
}
|
|
565
|
-
/**
|
|
566
|
-
* The tracker/PR ref for a session's dedicated column: the ticket id when known,
|
|
567
|
-
* else `PR#<n>`, else empty. Pulled out of the trailing badge blob so refs align
|
|
568
|
-
* into a scannable column instead of jamming against a truncated topic.
|
|
569
|
-
*/
|
|
570
244
|
export function ticketLabel(s) {
|
|
571
245
|
return s.ticketId ?? (s.prNumber ? `PR#${s.prNumber}` : '');
|
|
572
246
|
}
|
|
@@ -585,7 +259,6 @@ export function linkCwdCell(s, label) {
|
|
|
585
259
|
function modelLabel(model) {
|
|
586
260
|
return model ? shortenModel(model) : '-';
|
|
587
261
|
}
|
|
588
|
-
/** Canonical JSON row consumed by resume pickers and other session UIs. */
|
|
589
262
|
export function serializeSessionPickerRows(sessions, liveSessions, self = machineId()) {
|
|
590
263
|
const liveById = new Map(liveSessions.filter((row) => row.sessionId).map((row) => [row.sessionId, row]));
|
|
591
264
|
return sessions.map((session) => {
|
|
@@ -608,389 +281,6 @@ export function serializeSessionPickerRows(sessions, liveSessions, self = machin
|
|
|
608
281
|
};
|
|
609
282
|
});
|
|
610
283
|
}
|
|
611
|
-
/**
|
|
612
|
-
* Compact, colour-coded badges for the durable/awaiting signals. Text-only (no
|
|
613
|
-
* emoji, per repo convention): `plan` / `ask` / `perm` for why it's waiting,
|
|
614
|
-
* `PR#N`, `wt:slug`, `TICKET-123`.
|
|
615
|
-
*/
|
|
616
|
-
function signalBadges(s) {
|
|
617
|
-
const parts = [];
|
|
618
|
-
if (s.awaitingReason === 'plan_review')
|
|
619
|
-
parts.push(chalk.yellow('plan'));
|
|
620
|
-
else if (s.awaitingReason === 'question')
|
|
621
|
-
parts.push(chalk.yellow('ask'));
|
|
622
|
-
else if (s.awaitingReason === 'permission')
|
|
623
|
-
parts.push(chalk.yellow('perm'));
|
|
624
|
-
if (s.ticket) {
|
|
625
|
-
// Clickable when Linear workspace is resolvable (same helper as the picker header).
|
|
626
|
-
const url = linearIssueUrl(s.ticket.id);
|
|
627
|
-
parts.push(chalk.cyan(url ? linkUrl(url, s.ticket.id) : s.ticket.id));
|
|
628
|
-
}
|
|
629
|
-
if (s.pr) {
|
|
630
|
-
const label = `PR#${s.pr.number ?? '?'}`;
|
|
631
|
-
parts.push(chalk.blue(s.pr.url ? linkUrl(s.pr.url, label) : label));
|
|
632
|
-
}
|
|
633
|
-
if (s.worktree)
|
|
634
|
-
parts.push(chalk.magenta(`wt:${s.worktree.slug}`));
|
|
635
|
-
return parts.join(' ');
|
|
636
|
-
}
|
|
637
|
-
/**
|
|
638
|
-
* Compact locator badge: how to JUMP to the session, not what it's doing.
|
|
639
|
-
* `ssh` flags a remote host. For tmux, prefer the resolved `session:window.pane`
|
|
640
|
-
* (a real `tmux attach -t <session:window>` target) over the raw `%pane` id. For
|
|
641
|
-
* a local Ghostty session we know the tab, show `tab N`. Local, unlocatable
|
|
642
|
-
* sessions add nothing (the common case).
|
|
643
|
-
*
|
|
644
|
-
* RUSH-2336: every row also carries its raw process/provider handle — a
|
|
645
|
-
* `machine:pid` for a real OS process (terminal/tmux/headless/team), or
|
|
646
|
-
* `provider · taskId` for a cloud row with no local pid — so the human view
|
|
647
|
-
* exposes the same locator `--json` now guarantees on every active row.
|
|
648
|
-
*/
|
|
649
|
-
function locatorBadge(s) {
|
|
650
|
-
const p = s.provenance;
|
|
651
|
-
const parts = [];
|
|
652
|
-
// An ssh-launched session shows where it was launched FROM when the client IP
|
|
653
|
-
// resolves to a registered device (`ssh←zion`); bare `ssh` when it doesn't.
|
|
654
|
-
if (p?.transport === 'ssh')
|
|
655
|
-
parts.push(chalk.red(p.origin ? `ssh←${p.origin.device}` : 'ssh'));
|
|
656
|
-
if (p?.mux?.kind === 'tmux' && (s.tmuxTarget || p.mux.pane)) {
|
|
657
|
-
parts.push(chalk.green(s.tmuxTarget ?? p.mux.pane));
|
|
658
|
-
// For a tmux-hosted session, say which app+tab is looking at it right now
|
|
659
|
-
// (or that it's running detached). Only meaningful for tmux (the pane is the
|
|
660
|
-
// durable handle; the viewer is transient).
|
|
661
|
-
const label = viewingInLabel(s);
|
|
662
|
-
if (label)
|
|
663
|
-
parts.push(chalk.gray(label === 'detached' ? label : `viewing in ${label}`));
|
|
664
|
-
}
|
|
665
|
-
else if (p?.mux?.kind === 'screen') {
|
|
666
|
-
parts.push(chalk.green('screen'));
|
|
667
|
-
}
|
|
668
|
-
if (s.ghosttyTab != null)
|
|
669
|
-
parts.push(chalk.green(`tab ${s.ghosttyTab}`));
|
|
670
|
-
if (s.context === 'cloud') {
|
|
671
|
-
const bits = [s.cloudProvider, s.cloudTaskId ? s.cloudTaskId.slice(0, 12) : undefined].filter(Boolean);
|
|
672
|
-
if (bits.length)
|
|
673
|
-
parts.push(chalk.dim(bits.join(' · ')));
|
|
674
|
-
}
|
|
675
|
-
else if (typeof s.pid === 'number' && s.pid > 0) {
|
|
676
|
-
parts.push(chalk.dim(`${s.machine ? `${s.machine}:` : ''}pid ${s.pid}`));
|
|
677
|
-
}
|
|
678
|
-
return parts.join(' ');
|
|
679
|
-
}
|
|
680
|
-
/**
|
|
681
|
-
* The `created X · idle Y` time cell for a live row (RUSH-2205). `created` is the
|
|
682
|
-
* age of the session start ({@link ActiveSession.startedAtMs}); `idle` is the age
|
|
683
|
-
* of the last transcript write ({@link ActiveSession.lastActivityMs}) — i.e. how
|
|
684
|
-
* long it has been quiet. Compact ("6d", "3h", "now") so the row stays width-safe;
|
|
685
|
-
* either half is omitted when its epoch is unknown.
|
|
686
|
-
*/
|
|
687
|
-
function activeTimeCell(s) {
|
|
688
|
-
const parts = [];
|
|
689
|
-
if (s.startedAtMs)
|
|
690
|
-
parts.push(`created ${formatCompactAge(new Date(s.startedAtMs).toISOString())}`);
|
|
691
|
-
if (s.lastActivityMs)
|
|
692
|
-
parts.push(`idle ${formatCompactAge(new Date(s.lastActivityMs).toISOString())}`);
|
|
693
|
-
return parts.join(' · ');
|
|
694
|
-
}
|
|
695
|
-
/** Column widths for line 1 of an active row (id · agent · version · status · owner). */
|
|
696
|
-
const ROW_ID_W = 9;
|
|
697
|
-
const ROW_AGENT_W = 8;
|
|
698
|
-
const ROW_VERSION_W = 8;
|
|
699
|
-
const ROW_STATUS_W = 9;
|
|
700
|
-
const ROW_OWNER_W = 9;
|
|
701
|
-
/**
|
|
702
|
-
* Fit pre-styled content (which may carry SGR colour AND OSC-8 hyperlinks — the
|
|
703
|
-
* clickable ticket/PR/project badges) into `room` display cells. Returns it raw
|
|
704
|
-
* when it already fits, preserving colour and clickable links; otherwise falls
|
|
705
|
-
* back to a width-safe truncation. `truncateToWidth` strips OSC-8 before cutting
|
|
706
|
-
* (see width.ts), so a too-narrow cell drops the hyperlink cleanly rather than
|
|
707
|
-
* slicing a hyperlink escape in half and corrupting the terminal (RUSH-2205
|
|
708
|
-
* review). Never run pre-linked content through `truncateToWidth` directly — that
|
|
709
|
-
* strips its links even when it fits; go through this helper.
|
|
710
|
-
*/
|
|
711
|
-
function fitCell(content, room) {
|
|
712
|
-
if (room <= 0)
|
|
713
|
-
return '';
|
|
714
|
-
return stringWidth(content) <= room ? content : truncateToWidth(content, room);
|
|
715
|
-
}
|
|
716
|
-
/**
|
|
717
|
-
* Build the (one or two) rendered lines for a single active-session row.
|
|
718
|
-
* Indent is the leading whitespace (2 spaces for flat groups, 4 inside a window
|
|
719
|
-
* sub-group). Sized to `termW` so no line ever wraps under tmux/SSH (RUSH-2205):
|
|
720
|
-
*
|
|
721
|
-
* line 1: id · agent version · status · owner · created X · idle Y · ticket/PR
|
|
722
|
-
* line 2: └ label/topic (+ checklist) · jump locator (ssh/tmux/detached)
|
|
723
|
-
*
|
|
724
|
-
* The label/topic gets its own line so it is no longer buried in a truncated grey
|
|
725
|
-
* snippet, and the actionable ticket/PR badges ride line 1. Version and the
|
|
726
|
-
* ticket/PR/label are backfilled onto the {@link ActiveSession} from the indexed
|
|
727
|
-
* SessionMeta before this renders (a live process reports none of them). Pure +
|
|
728
|
-
* exported so the row layout is unit-tested for content and width without a
|
|
729
|
-
* captured stdout.
|
|
730
|
-
*/
|
|
731
|
-
export function renderActiveRowLines(s, indent, termW) {
|
|
732
|
-
const idCol = chalk.dim(padToWidth((s.sessionId?.slice(0, 8)) ?? '-', ROW_ID_W));
|
|
733
|
-
const shownKind = sessionDisplayAgent({ agent: s.kind, harness: s.harness });
|
|
734
|
-
const kindCol = colorAgent(shownKind)(padToWidth(truncateToWidth(shownKind, ROW_AGENT_W), ROW_AGENT_W + 1));
|
|
735
|
-
const versionCol = chalk.gray(padToWidth(truncateToWidth(s.version ?? '', ROW_VERSION_W), ROW_VERSION_W + 1));
|
|
736
|
-
const statusCol = statusColor(s.status)(padToWidth(truncateToWidth(activityLabel(s), ROW_STATUS_W - 1), ROW_STATUS_W));
|
|
737
|
-
const ownerCol = chalk.cyan(padToWidth(truncateToWidth(ownerLabel(s), ROW_OWNER_W - 1), ROW_OWNER_W));
|
|
738
|
-
const fixedCols = idCol + kindCol + versionCol + statusCol + ownerCol;
|
|
739
|
-
const fixedW = stringWidth(indent) + ROW_ID_W + (ROW_AGENT_W + 1) + (ROW_VERSION_W + 1) + ROW_STATUS_W + ROW_OWNER_W;
|
|
740
|
-
const remaining = Math.max(0, termW - fixedW - 1);
|
|
741
|
-
// Line 1 right side: created/idle time + actionable badges (fork count,
|
|
742
|
-
// plan/ask/perm, ticket, PR, worktree). Badges are the jump-to-work signal, so
|
|
743
|
-
// they win the width fight — fitCell keeps them raw+clickable when they fit and
|
|
744
|
-
// drops the link cleanly when narrow; the time cell takes whatever is left.
|
|
745
|
-
const fork = s.pidCount && s.pidCount > 1 ? chalk.dim(`×${s.pidCount} `) : '';
|
|
746
|
-
const badgesCell = fitCell(fork + signalBadges(s), remaining);
|
|
747
|
-
const badgesW = stringWidth(badgesCell);
|
|
748
|
-
const timeRoom = Math.max(0, remaining - (badgesW ? badgesW + 2 : 0));
|
|
749
|
-
const timeCell = chalk.gray(truncateToWidth(activeTimeCell(s), timeRoom));
|
|
750
|
-
let right = timeCell;
|
|
751
|
-
if (badgesCell)
|
|
752
|
-
right += (stringWidth(timeCell) ? ' ' : '') + badgesCell;
|
|
753
|
-
// right's visible width is <= remaining by construction, so the assembled line
|
|
754
|
-
// is <= termW-1 whenever the fixed columns fit; the clamp only bites a terminal
|
|
755
|
-
// narrower than the fixed columns (where right is empty — no link to corrupt).
|
|
756
|
-
let line1 = indent + fixedCols + right;
|
|
757
|
-
if (stringWidth(line1) > termW)
|
|
758
|
-
line1 = truncateToWidth(line1, termW);
|
|
759
|
-
const lines = [line1];
|
|
760
|
-
// Line 2: label/topic + checklist (the identity, no longer buried) then the
|
|
761
|
-
// jump locator. Skipped entirely when there is nothing to say. desc may carry a
|
|
762
|
-
// clickable project link, so it also goes through fitCell.
|
|
763
|
-
const contIndent = indent + ' '.repeat(ROW_ID_W);
|
|
764
|
-
const desc = formatActiveRowDescription(s);
|
|
765
|
-
const loc = locatorBadge(s);
|
|
766
|
-
if (desc || loc) {
|
|
767
|
-
const room2 = Math.max(0, termW - stringWidth(contIndent) - 2);
|
|
768
|
-
const locCell = fitCell(loc, room2);
|
|
769
|
-
const locW = stringWidth(locCell);
|
|
770
|
-
const descRoom = Math.max(0, room2 - (locW ? locW + 2 : 0));
|
|
771
|
-
const descCell = chalk.white(fitCell(desc || '-', descRoom));
|
|
772
|
-
let line2 = contIndent + chalk.dim('└ ') + descCell;
|
|
773
|
-
if (locCell)
|
|
774
|
-
line2 += ' ' + locCell;
|
|
775
|
-
if (stringWidth(line2) > termW)
|
|
776
|
-
line2 = truncateToWidth(line2, termW);
|
|
777
|
-
lines.push(line2);
|
|
778
|
-
}
|
|
779
|
-
// Secondary line (PHNX-3797 owner feedback): surface the most important recent
|
|
780
|
-
// agent message when it is a pending QUESTION or a NEEDS-YOU block — the urgency
|
|
781
|
-
// a rolling activity preview buries. Plain `activity` is already the preview on
|
|
782
|
-
// line 2, so it earns no extra line here; the full ranked message still rides
|
|
783
|
-
// the JSON/mirror feed for AGI EXT via `s.importantMessage`.
|
|
784
|
-
const important = s.importantMessage;
|
|
785
|
-
if (important && (important.kind === 'question' || important.kind === 'needs_you')) {
|
|
786
|
-
const glyph = important.kind === 'question' ? '? ' : '! ';
|
|
787
|
-
const room3 = Math.max(0, termW - stringWidth(contIndent) - 2 - glyph.length);
|
|
788
|
-
const msgCell = fitCell(cleanPreview(important.text), room3);
|
|
789
|
-
if (msgCell) {
|
|
790
|
-
let line3 = contIndent + chalk.dim(glyph + msgCell);
|
|
791
|
-
if (stringWidth(line3) > termW)
|
|
792
|
-
line3 = truncateToWidth(line3, termW);
|
|
793
|
-
lines.push(line3);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
return lines;
|
|
797
|
-
}
|
|
798
|
-
/** Render a single agent-session row inside an already-printed group header. */
|
|
799
|
-
function printActiveRow(s, indent) {
|
|
800
|
-
for (const line of renderActiveRowLines(s, indent, terminalWidth()))
|
|
801
|
-
console.log(line);
|
|
802
|
-
}
|
|
803
|
-
/**
|
|
804
|
-
* Compact owner display for the `--active` owner column: the local-part of a
|
|
805
|
-
* resolved actor email/login (`muqsit@getrush.ai` -> `muqsit`), the id as-is
|
|
806
|
-
* when it has no `@`, and `-` when the actor is unresolved (`UNRESOLVED@<host>`)
|
|
807
|
-
* or absent (a launch predating actor stamping). Honest by design — an
|
|
808
|
-
* unresolved local run shows no owner rather than inventing one.
|
|
809
|
-
*/
|
|
810
|
-
export function ownerLabel(s) {
|
|
811
|
-
const owner = s.owner;
|
|
812
|
-
if (!owner || owner.startsWith('UNRESOLVED@'))
|
|
813
|
-
return '-';
|
|
814
|
-
const at = owner.indexOf('@');
|
|
815
|
-
return at > 0 ? owner.slice(0, at) : owner;
|
|
816
|
-
}
|
|
817
|
-
/**
|
|
818
|
-
* Short label for an IDE window. The slice key in live-terminals.json is
|
|
819
|
-
* `${vscode.env.sessionId}-${ext-host pid}`; the trailing pid is the cheap
|
|
820
|
-
* stable disambiguator. We surface it as `ext-pid` so two windows on the
|
|
821
|
-
* same repo are visibly different.
|
|
822
|
-
*/
|
|
823
|
-
function shortWindowLabel(windowId) {
|
|
824
|
-
const m = windowId.match(/-(\d+)$/);
|
|
825
|
-
return m ? `ext-pid ${m[1]}` : `win ${windowId.slice(0, 8)}`;
|
|
826
|
-
}
|
|
827
|
-
/**
|
|
828
|
-
* Group sessions by workspace, then split each workspace into IDE-window
|
|
829
|
-
* sub-groups + a flat bucket. Pure function — no I/O — so the renderer's
|
|
830
|
-
* grouping rules can be tested without mocking the session scanner.
|
|
831
|
-
*
|
|
832
|
-
* Sort order:
|
|
833
|
-
* - workspaces: by session count descending, then key ascending
|
|
834
|
-
* - windows within a workspace: by oldest startedAtMs ascending
|
|
835
|
-
* - sessions within a window/flat bucket: input order preserved
|
|
836
|
-
*/
|
|
837
|
-
export function groupActiveSessions(sessions) {
|
|
838
|
-
const byWorkspace = new Map();
|
|
839
|
-
for (const s of sessions) {
|
|
840
|
-
const key = s.cwd ?? (s.context === 'cloud' ? '__cloud__' : '__unknown__');
|
|
841
|
-
const list = byWorkspace.get(key) || [];
|
|
842
|
-
list.push(s);
|
|
843
|
-
byWorkspace.set(key, list);
|
|
844
|
-
}
|
|
845
|
-
const sortedKeys = Array.from(byWorkspace.keys()).sort((a, b) => {
|
|
846
|
-
const aCount = byWorkspace.get(a).length;
|
|
847
|
-
const bCount = byWorkspace.get(b).length;
|
|
848
|
-
if (aCount !== bCount)
|
|
849
|
-
return bCount - aCount;
|
|
850
|
-
return a.localeCompare(b);
|
|
851
|
-
});
|
|
852
|
-
const workspaces = sortedKeys.map((key) => {
|
|
853
|
-
const group = byWorkspace.get(key);
|
|
854
|
-
const windowedSessions = [];
|
|
855
|
-
const flat = [];
|
|
856
|
-
for (const s of group) {
|
|
857
|
-
if (s.context === 'terminal' && s.windowId)
|
|
858
|
-
windowedSessions.push(s);
|
|
859
|
-
else
|
|
860
|
-
flat.push(s);
|
|
861
|
-
}
|
|
862
|
-
const byWindow = new Map();
|
|
863
|
-
for (const s of windowedSessions) {
|
|
864
|
-
const list = byWindow.get(s.windowId) || [];
|
|
865
|
-
list.push(s);
|
|
866
|
-
byWindow.set(s.windowId, list);
|
|
867
|
-
}
|
|
868
|
-
const windowKeys = Array.from(byWindow.keys()).sort((a, b) => {
|
|
869
|
-
const aStart = Math.min(...byWindow.get(a).map(s => s.startedAtMs ?? Infinity));
|
|
870
|
-
const bStart = Math.min(...byWindow.get(b).map(s => s.startedAtMs ?? Infinity));
|
|
871
|
-
return aStart - bStart;
|
|
872
|
-
});
|
|
873
|
-
return {
|
|
874
|
-
key,
|
|
875
|
-
total: group.length,
|
|
876
|
-
windows: windowKeys.map((wid) => ({ windowId: wid, sessions: byWindow.get(wid) })),
|
|
877
|
-
flat,
|
|
878
|
-
};
|
|
879
|
-
});
|
|
880
|
-
return { workspaces };
|
|
881
|
-
}
|
|
882
|
-
/**
|
|
883
|
-
* The machine a session belongs to: an explicit tag (set when merging
|
|
884
|
-
* cross-machine results) wins; else the process's provenance host (normalized
|
|
885
|
-
* to the same id form); else the local machine. Never keys off `ActiveSession.host`
|
|
886
|
-
* — that is the terminal *app* (code/tmux), not the computer.
|
|
887
|
-
*/
|
|
888
|
-
/** Synthetic top-level group key for provider-sandboxed cloud tasks. */
|
|
889
|
-
const CLOUD_MACHINE_KEY = 'cloud';
|
|
890
|
-
function machineKeyFor(s, localMachine) {
|
|
891
|
-
// Cloud tasks run in a provider sandbox, not on the machine they're attributed
|
|
892
|
-
// to for reply routing (s.machine = the querier). Surface them as their own
|
|
893
|
-
// top-level "cloud" group instead of nested under the local device.
|
|
894
|
-
if (s.context === 'cloud')
|
|
895
|
-
return CLOUD_MACHINE_KEY;
|
|
896
|
-
if (s.machine)
|
|
897
|
-
return s.machine;
|
|
898
|
-
if (s.provenance?.host)
|
|
899
|
-
return normalizeHost(s.provenance.host);
|
|
900
|
-
return localMachine;
|
|
901
|
-
}
|
|
902
|
-
/**
|
|
903
|
-
* Group active sessions by machine, then delegate each machine's sessions to the
|
|
904
|
-
* existing workspace/window grouping. Local machine is pinned first and flagged;
|
|
905
|
-
* the rest sort by session count descending, then name. Pure — `localMachine` is
|
|
906
|
-
* injected so the function stays testable without reading os.hostname().
|
|
907
|
-
*/
|
|
908
|
-
export function groupSessionsByMachine(sessions, localMachine) {
|
|
909
|
-
const byMachine = new Map();
|
|
910
|
-
for (const s of sessions) {
|
|
911
|
-
const key = machineKeyFor(s, localMachine);
|
|
912
|
-
(byMachine.get(key) ?? byMachine.set(key, []).get(key)).push(s);
|
|
913
|
-
}
|
|
914
|
-
const keys = Array.from(byMachine.keys()).sort((a, b) => {
|
|
915
|
-
if (a === localMachine)
|
|
916
|
-
return -1;
|
|
917
|
-
if (b === localMachine)
|
|
918
|
-
return 1;
|
|
919
|
-
// The synthetic "cloud" category sorts after all real machines.
|
|
920
|
-
if (a === CLOUD_MACHINE_KEY)
|
|
921
|
-
return 1;
|
|
922
|
-
if (b === CLOUD_MACHINE_KEY)
|
|
923
|
-
return -1;
|
|
924
|
-
const ac = byMachine.get(a).length, bc = byMachine.get(b).length;
|
|
925
|
-
if (ac !== bc)
|
|
926
|
-
return bc - ac;
|
|
927
|
-
return a.localeCompare(b);
|
|
928
|
-
});
|
|
929
|
-
const machines = keys.map((machine) => ({
|
|
930
|
-
machine,
|
|
931
|
-
isLocal: machine === localMachine,
|
|
932
|
-
total: byMachine.get(machine).length,
|
|
933
|
-
layout: groupActiveSessions(byMachine.get(machine)),
|
|
934
|
-
}));
|
|
935
|
-
return { machines };
|
|
936
|
-
}
|
|
937
|
-
/**
|
|
938
|
-
* Collapse duplicate sessions after a cross-machine merge. Two rows collapse
|
|
939
|
-
* only when they share both machine and session UUID (the same host listed
|
|
940
|
-
* twice, or a local/remote overlap); rows without a sessionId can't be
|
|
941
|
-
* correlated, so they're all kept.
|
|
942
|
-
*/
|
|
943
|
-
export function dedupeByMachineSession(sessions) {
|
|
944
|
-
const seen = new Map();
|
|
945
|
-
const out = [];
|
|
946
|
-
for (const s of sessions) {
|
|
947
|
-
if (!s.sessionId) {
|
|
948
|
-
out.push(s);
|
|
949
|
-
continue;
|
|
950
|
-
}
|
|
951
|
-
const key = `${s.machine ?? ''}:${s.sessionId}`;
|
|
952
|
-
const at = seen.get(key);
|
|
953
|
-
if (at === undefined) {
|
|
954
|
-
seen.set(key, out.length);
|
|
955
|
-
out.push(s);
|
|
956
|
-
continue;
|
|
957
|
-
}
|
|
958
|
-
// Both rows now describe the same session on the same machine, so the tie is
|
|
959
|
-
// between the EXECUTING box's own row and the dispatcher's shim row for a
|
|
960
|
-
// host-dispatched run (they only started colliding once foldExecutionMachine
|
|
961
|
-
// attributed both to the executing host — RUSH-2479). The shim carries no
|
|
962
|
-
// transcript and a `[host/<peer>]` placeholder label, so keeping it first
|
|
963
|
-
// would strip a real preview off the merged fleet view. Prefer the row that
|
|
964
|
-
// is not an offload shim; otherwise the first one still wins.
|
|
965
|
-
if (out[at].offloadedFrom && !s.offloadedFrom)
|
|
966
|
-
out[at] = s;
|
|
967
|
-
}
|
|
968
|
-
return out;
|
|
969
|
-
}
|
|
970
|
-
/**
|
|
971
|
-
* Narrow a gathered live set to the machines an explicit `--device`
|
|
972
|
-
* scope named. The gather picks which boxes to ASK; this asserts what the answer
|
|
973
|
-
* may contain, and the two are not the same question: a host-dispatched run is
|
|
974
|
-
* reported by the box that dispatched it while executing somewhere else, so
|
|
975
|
-
* `--device <dispatcher>` used to list sessions that were not running there at
|
|
976
|
-
* all (RUSH-2479). No scope → unchanged. Pure; exported for unit testing.
|
|
977
|
-
*/
|
|
978
|
-
export function filterActiveSessionsByHostScope(sessions, hosts, self) {
|
|
979
|
-
if (!hosts || hosts.length === 0)
|
|
980
|
-
return sessions;
|
|
981
|
-
const wanted = new Set(hosts.map(hostToken));
|
|
982
|
-
return sessions.filter((s) => wanted.has(s.machine ?? self));
|
|
983
|
-
}
|
|
984
|
-
/**
|
|
985
|
-
* Order a merged listing so the local machine's sessions come first, then each
|
|
986
|
-
* remote machine as a contiguous block (more sessions first, then name), with
|
|
987
|
-
* every machine keeping its incoming order (timestamp) within the block. Also
|
|
988
|
-
* dedupes: a session present both locally (a synced mirror copy) and via live
|
|
989
|
-
* fan-out collapses to one, keyed by machine + session id. Rows are keyed by
|
|
990
|
-
* `machine` (discover tags local rows with the local id; fan-out tags remote
|
|
991
|
-
* rows with the peer id) falling back to `localMachine` when untagged. Pure —
|
|
992
|
-
* `localMachine` is injected so the ordering is testable without os.hostname().
|
|
993
|
-
*/
|
|
994
284
|
export function mergeLocalFirst(sessions, localMachine) {
|
|
995
285
|
const byMachine = new Map();
|
|
996
286
|
const seen = new Set();
|
|
@@ -1016,17 +306,7 @@ export function mergeLocalFirst(sessions, localMachine) {
|
|
|
1016
306
|
});
|
|
1017
307
|
return keys.flatMap((k) => byMachine.get(k));
|
|
1018
308
|
}
|
|
1019
|
-
|
|
1020
|
-
* Serialize a `SessionMeta[]` to the clean JSON shape the `--json` listing
|
|
1021
|
-
* emits: strip the internal-only scoring/provenance fields (`_matchedTerms`,
|
|
1022
|
-
* `_bm25Score`, `_remote`) that are search/fan-out bookkeeping, never part of
|
|
1023
|
-
* the public record, then pretty-print as a 2-space array with a trailing
|
|
1024
|
-
* newline. The single seam shared by the local `--json` path and the
|
|
1025
|
-
* `--json --host` remote fan-out so both emit byte-identical row shapes.
|
|
1026
|
-
*/
|
|
1027
|
-
/** The intentionally small metadata contract emitted by `sessions --resolve`.
|
|
1028
|
-
* It includes only launch identity needed to route/resume. Transcript paths,
|
|
1029
|
-
* extracted plans, costs, and content stay on the machine that owns them. */
|
|
309
|
+
// Peer resolution exposes launch identity only; paths, plans, costs, and content stay local.
|
|
1030
310
|
export function serializeResolvedSessionsJson(sessions) {
|
|
1031
311
|
const safe = sessions.map((session) => ({
|
|
1032
312
|
id: session.id,
|
|
@@ -1045,30 +325,13 @@ export function serializeResolvedSessionsJson(sessions) {
|
|
|
1045
325
|
}));
|
|
1046
326
|
return JSON.stringify(safe, null, 2) + '\n';
|
|
1047
327
|
}
|
|
1048
|
-
/**
|
|
1049
|
-
* `agents sessions --json --host <h>` — fan the RECENT (non-active) listing out
|
|
1050
|
-
* to the named host(s) and emit ONE clean merged `SessionMeta[]` JSON array,
|
|
1051
|
-
* the same shape the local `--json` path emits. Reuses `gatherRemoteList` (the
|
|
1052
|
-
* exact SSH fan-out the interactive cross-machine listing already uses) and
|
|
1053
|
-
* serializes the merged, machine-tagged rows — instead of `runRemoteSessions`,
|
|
1054
|
-
* which streams each remote's raw stdout under a per-host banner and so can
|
|
1055
|
-
* never be JSON.parsed. A dead host contributes `[]` (with a stderr note from
|
|
1056
|
-
* the fan-out), so stdout is always a valid array and the exit stays 0.
|
|
1057
|
-
*/
|
|
1058
328
|
async function runRemoteSessionsJson(hosts) {
|
|
1059
|
-
// Forward the caller's own filters (query, --limit, --since, …) minus --host,
|
|
1060
|
-
// and guarantee --json so each peer answers with a parseable array. Force
|
|
1061
|
-
// whole-index scope: an explicit --host means "that box's index", not the
|
|
1062
|
-
// slice that happens to sit under the peer's SSH-login home dir.
|
|
1063
329
|
const forwarded = ensureWholeIndex(buildForwardedArgs(process.argv, new Set(hosts)));
|
|
1064
330
|
if (!forwarded.includes('--json'))
|
|
1065
331
|
forwarded.push('--json');
|
|
1066
332
|
const { sessions } = await gatherRemoteList(forwarded, hosts);
|
|
1067
333
|
process.stdout.write(serializeSessionsJson(sessions));
|
|
1068
334
|
}
|
|
1069
|
-
/** Parse one peer's computer-run array without accepting banners or partial
|
|
1070
|
-
* JSON. The fan-out boundary supplies the machine name when an older peer row
|
|
1071
|
-
* omitted it. */
|
|
1072
335
|
export function parseRemoteComputerSessionRows(stdout, machine) {
|
|
1073
336
|
try {
|
|
1074
337
|
const value = JSON.parse(stdout);
|
|
@@ -1083,9 +346,6 @@ export function parseRemoteComputerSessionRows(stdout, machine) {
|
|
|
1083
346
|
return { items: [], valid: false };
|
|
1084
347
|
}
|
|
1085
348
|
}
|
|
1086
|
-
/** Collect computer history from named peers, or every automatic peer when
|
|
1087
|
-
* hosts is omitted. Every peer is forced to JSON and receives the recursion
|
|
1088
|
-
* guard through the shared fleet transport. */
|
|
1089
349
|
async function gatherRemoteComputerSessionRows(hosts) {
|
|
1090
350
|
const hostSet = new Set(hosts ?? []);
|
|
1091
351
|
const forwarded = buildForwardedArgs(process.argv, hostSet);
|
|
@@ -1101,515 +361,31 @@ async function gatherRemoteComputerSessionRows(hosts) {
|
|
|
1101
361
|
});
|
|
1102
362
|
return result.items;
|
|
1103
363
|
}
|
|
1104
|
-
/**
|
|
1105
|
-
* `running N · idle N · waiting N · queued N · closed N · abandoned N · unknown N`
|
|
1106
|
-
* for a bucket of sessions (zero buckets omitted). Same bucketing as the summary so
|
|
1107
|
-
* per-group counts reconcile with the `(total)` beside the header — the `unknown`
|
|
1108
|
-
* bucket is what keeps an alive-but-opaque row from silently vanishing from the
|
|
1109
|
-
* tally. Empty when nothing.
|
|
1110
|
-
*/
|
|
1111
|
-
function groupTally(sessions) {
|
|
1112
|
-
const running = sessions.filter(s => s.status === 'running').length;
|
|
1113
|
-
const idle = sessions.filter(s => s.status === 'idle').length;
|
|
1114
|
-
const waiting = sessions.filter(s => s.status === 'input_required').length;
|
|
1115
|
-
// Counted by status, deliberately: an orphaned session gets its own bucket
|
|
1116
|
-
// below, so counting it here too would double-count it in the tally.
|
|
1117
|
-
const queued = sessions.filter(s => s.status === 'queued').length;
|
|
1118
|
-
const closed = sessions.filter(s => s.status === 'closed').length;
|
|
1119
|
-
const abandoned = sessions.filter(s => s.status === 'abandoned').length;
|
|
1120
|
-
// Without these two the tally silently loses rows: every status must have a
|
|
1121
|
-
// bucket or "N active (…)" stops adding up to what the list shows.
|
|
1122
|
-
const orphaned = sessions.filter(s => s.status === 'orphaned').length;
|
|
1123
|
-
const crashed = sessions.filter(s => s.status === 'crashed').length;
|
|
1124
|
-
const unknown = sessions.filter(s => s.status === 'unknown').length;
|
|
1125
|
-
const parts = [];
|
|
1126
|
-
if (running)
|
|
1127
|
-
parts.push(`${running} running`);
|
|
1128
|
-
if (idle)
|
|
1129
|
-
parts.push(`${idle} idle`);
|
|
1130
|
-
if (waiting)
|
|
1131
|
-
parts.push(`${waiting} waiting`);
|
|
1132
|
-
if (queued)
|
|
1133
|
-
parts.push(`${queued} queued`);
|
|
1134
|
-
if (closed)
|
|
1135
|
-
parts.push(`${closed} closed`);
|
|
1136
|
-
if (abandoned)
|
|
1137
|
-
parts.push(`${abandoned} abandoned`);
|
|
1138
|
-
if (orphaned)
|
|
1139
|
-
parts.push(`${orphaned} orphaned`);
|
|
1140
|
-
if (crashed)
|
|
1141
|
-
parts.push(`${crashed} crashed`);
|
|
1142
|
-
if (unknown)
|
|
1143
|
-
parts.push(`${unknown} unknown`);
|
|
1144
|
-
return parts.join(' · ');
|
|
1145
|
-
}
|
|
1146
|
-
/** Print one machine's workspace tree, indented under its machine header. */
|
|
1147
|
-
function renderWorkspaceLayout(layout, base, machineKey) {
|
|
1148
|
-
let first = true;
|
|
1149
|
-
for (const ws of layout.workspaces) {
|
|
1150
|
-
if (!first)
|
|
1151
|
-
console.log();
|
|
1152
|
-
first = false;
|
|
1153
|
-
// Under the top-level "cloud" machine group the __cloud__ workspace header is
|
|
1154
|
-
// redundant ("▸ cloud" then "cloud") — render its rows flat under the machine
|
|
1155
|
-
// header instead. Row indent collapses by one level to match.
|
|
1156
|
-
const redundantCloud = ws.key === '__cloud__' && machineKey === CLOUD_MACHINE_KEY;
|
|
1157
|
-
const rowBase = redundantCloud ? base : base + ' ';
|
|
1158
|
-
if (!redundantCloud) {
|
|
1159
|
-
const header = ws.key === '__cloud__'
|
|
1160
|
-
? chalk.magenta.bold('cloud')
|
|
1161
|
-
: ws.key === '__unknown__'
|
|
1162
|
-
? chalk.gray.bold('unknown')
|
|
1163
|
-
: chalk.cyan.bold(shortCwd(ws.key));
|
|
1164
|
-
const wsSessions = [...ws.windows.flatMap(w => w.sessions), ...ws.flat];
|
|
1165
|
-
const tally = groupTally(wsSessions);
|
|
1166
|
-
console.log(`${base}${header} ${chalk.gray(`(${ws.total})`)}${tally ? chalk.gray(` ${tally}`) : ''}`);
|
|
1167
|
-
}
|
|
1168
|
-
for (const win of ws.windows) {
|
|
1169
|
-
// Host is per-process, but every terminal in the same IDE window shares
|
|
1170
|
-
// an ancestor — take the first non-empty host as the window's label.
|
|
1171
|
-
const host = win.sessions.find((s) => s.host)?.host ?? 'terminal';
|
|
1172
|
-
const winHeader = `${chalk.gray(host)} ${chalk.gray('·')} ${chalk.gray(shortWindowLabel(win.windowId))} ${chalk.gray(`(${win.sessions.length})`)}`;
|
|
1173
|
-
console.log(rowBase + winHeader);
|
|
1174
|
-
for (const s of win.sessions)
|
|
1175
|
-
printActiveRow(s, rowBase + ' ');
|
|
1176
|
-
}
|
|
1177
|
-
for (const s of ws.flat)
|
|
1178
|
-
printActiveRow(s, rowBase);
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
/** Machine header: `▸ <name> ← this machine` for the local box (cyan), matching
|
|
1182
|
-
* the `ag devices list` treatment; a plain `▸ <name>` for remotes. */
|
|
1183
|
-
function printMachineHeader(mg) {
|
|
1184
|
-
// The synthetic "cloud" group isn't a device — tint it magenta (matching the
|
|
1185
|
-
// cloud row/label styling) so it reads as a category, not a machine.
|
|
1186
|
-
const isCloud = mg.machine === CLOUD_MACHINE_KEY;
|
|
1187
|
-
const marker = mg.isLocal ? chalk.cyan('▸ ') : isCloud ? chalk.magenta('▸ ') : chalk.gray('▸ ');
|
|
1188
|
-
const name = mg.isLocal ? chalk.bold.cyan(mg.machine) : isCloud ? chalk.bold.magenta(mg.machine) : chalk.bold(mg.machine);
|
|
1189
|
-
const here = mg.isLocal ? chalk.cyan(' ← this machine') : '';
|
|
1190
|
-
console.log(`${marker}${name} ${chalk.gray(`(${mg.total})`)}${here}`);
|
|
1191
|
-
}
|
|
1192
|
-
/**
|
|
1193
|
-
* Attach display-only jump locators onto LOCAL sessions: the Ghostty tab number
|
|
1194
|
-
* (one batched read-only osascript, only when a local ghostty session exists)
|
|
1195
|
-
* and the tmux `session:window.pane` target (one `list-panes -a` per socket).
|
|
1196
|
-
* Every step is best-effort and swallowed — a failure just leaves the raw pane
|
|
1197
|
-
* id / no tab number, and the rows render as before. Mutates the sessions.
|
|
1198
|
-
*/
|
|
1199
|
-
async function enrichLocalLocators(local) {
|
|
1200
|
-
// Ghostty tab numbers.
|
|
1201
|
-
try {
|
|
1202
|
-
const ghostty = local.filter(s => s.host === 'ghostty' && s.provenance?.transport !== 'ssh');
|
|
1203
|
-
if (ghostty.length > 0) {
|
|
1204
|
-
const surfaces = await enumerateGhosttyTabs();
|
|
1205
|
-
for (const [sess, tab] of assignGhosttyTabs(ghostty, surfaces))
|
|
1206
|
-
sess.ghosttyTab = tab;
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
catch { /* non-fatal */ }
|
|
1210
|
-
// One Ghostty enumeration shared across every socket's viewing-in resolve
|
|
1211
|
-
// (a tmux client can be attached from a Ghostty tab).
|
|
1212
|
-
await enrichTmuxLocators(local, await enumerateGhosttyTabsQuietly());
|
|
1213
|
-
}
|
|
1214
|
-
/** {@link enumerateGhosttyTabs}, best-effort — an osascript failure yields no surfaces. */
|
|
1215
|
-
async function enumerateGhosttyTabsQuietly() {
|
|
1216
|
-
try {
|
|
1217
|
-
return await enumerateGhosttyTabs();
|
|
1218
|
-
}
|
|
1219
|
-
catch {
|
|
1220
|
-
return [];
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
/**
|
|
1224
|
-
* The tmux half of {@link enrichLocalLocators}: the `session:window.pane` attach
|
|
1225
|
-
* target and "viewing in <app> tab N" / detached, one batched query per socket.
|
|
1226
|
-
*
|
|
1227
|
-
* Split out because it is the only locator the `--json` path can afford. It costs
|
|
1228
|
-
* tmux queries and a `ps` read — no osascript — so scriptable output stays cheap
|
|
1229
|
-
* while still answering the question a consumer actually needs: is anyone looking
|
|
1230
|
-
* at this session, or is it running orphaned? Without `surfaces`, a Ghostty-attached
|
|
1231
|
-
* client still resolves as attached, just without its tab number.
|
|
1232
|
-
*/
|
|
1233
|
-
async function enrichTmuxLocators(local, surfaces = []) {
|
|
1234
|
-
try {
|
|
1235
|
-
const tmux = local.filter(s => s.provenance?.mux?.kind === 'tmux' && s.provenance.mux.pane);
|
|
1236
|
-
if (tmux.length > 0) {
|
|
1237
|
-
const sockets = new Set(tmux.map(s => s.provenance.mux.socket));
|
|
1238
|
-
for (const socket of sockets) {
|
|
1239
|
-
const paneMap = await mapPanesToTargets(socket);
|
|
1240
|
-
if (paneMap.size === 0)
|
|
1241
|
-
continue;
|
|
1242
|
-
const clients = await listClients(socket);
|
|
1243
|
-
for (const s of tmux) {
|
|
1244
|
-
if (s.provenance.mux.socket !== socket)
|
|
1245
|
-
continue;
|
|
1246
|
-
const target = paneMap.get(s.provenance.mux.pane);
|
|
1247
|
-
if (target)
|
|
1248
|
-
s.tmuxTarget = target;
|
|
1249
|
-
s.viewingIn = await resolveViewingIn(s, clients, { paneToTarget: paneMap, ghosttySurfaces: surfaces });
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
catch { /* non-fatal */ }
|
|
1255
|
-
}
|
|
1256
|
-
/** Normalize a `--device` token (`alias`, `user@host`, `host.domain`)
|
|
1257
|
-
* to the machine id the fan-out and registry key off. */
|
|
1258
|
-
function hostToken(h) {
|
|
1259
|
-
return normalizeHost(h.split('@').pop() || h);
|
|
1260
|
-
}
|
|
1261
|
-
/**
|
|
1262
|
-
* Whether the local machine's sessions belong in an `--active` view. Local is
|
|
1263
|
-
* included by default; an explicit `--device` list scopes the view to
|
|
1264
|
-
* exactly those machines, so local is dropped unless it is itself named (by
|
|
1265
|
-
* alias or `user@host`, matched on the normalized machine id). Exported for
|
|
1266
|
-
* unit testing without touching SSH or the live process table.
|
|
1267
|
-
*/
|
|
1268
|
-
export function shouldIncludeLocal(hosts, self) {
|
|
1269
|
-
if (!hosts || hosts.length === 0)
|
|
1270
|
-
return true;
|
|
1271
|
-
return hosts.some(h => hostToken(h) === self);
|
|
1272
|
-
}
|
|
1273
|
-
/**
|
|
1274
|
-
* The peers to dial for an `--active` view. No `--device` → `undefined`, which
|
|
1275
|
-
* tells `gatherRemoteActive` to sweep the registered online devices. An
|
|
1276
|
-
* explicit list → exactly those, minus this machine (its sessions come from the
|
|
1277
|
-
* local seed, so dialing self would be a wasted SSH and a spurious "unreachable"
|
|
1278
|
-
* note). Returns `[]` when the only named host is self — the caller then skips
|
|
1279
|
-
* the remote fan-out entirely rather than letting `[]` trigger the sweep.
|
|
1280
|
-
* Exported for unit testing.
|
|
1281
|
-
*/
|
|
1282
|
-
export function remoteHostsToDial(hosts, self) {
|
|
1283
|
-
if (!hosts || hosts.length === 0)
|
|
1284
|
-
return undefined;
|
|
1285
|
-
return hosts.filter(h => hostToken(h) !== self);
|
|
1286
|
-
}
|
|
1287
|
-
/**
|
|
1288
|
-
* The fleet-wide live-session set behind every `--active` surface. Local sessions
|
|
1289
|
-
* come from `getActiveSessions()` and (unless `--local`) the registered online
|
|
1290
|
-
* devices from `ag devices` are folded in over SSH. An explicit `--device`
|
|
1291
|
-
* list SCOPES the sweep to exactly those machines — the local machine is included
|
|
1292
|
-
* only when it is itself named — so `--device` is a filter, not an addition.
|
|
1293
|
-
*
|
|
1294
|
-
* This is the single gather: the static renderer AND the interactive browser both
|
|
1295
|
-
* call it, so the browser can never disagree with `--active --json` about which
|
|
1296
|
-
* sessions are live (it used to call the local-only `getActiveSessions()` directly
|
|
1297
|
-
* and silently hid every remote session).
|
|
1298
|
-
*
|
|
1299
|
-
* RUSH-2062: default path is cache-first against the daemon-warmed shared
|
|
1300
|
-
* snapshot (`session-cache.ts`). Menubar / Factory / watchdog / CLI share one
|
|
1301
|
-
* warm result instead of each re-running the full SSH fan-out. `forceRefresh`
|
|
1302
|
-
* (or `AGENTS_SESSIONS_FORCE_REFRESH=1`) re-gathers live; scoped `--device` lists
|
|
1303
|
-
* always gather live so a filter never returns a wrong unscoped snapshot.
|
|
1304
|
-
*/
|
|
1305
|
-
export async function gatherActiveSessions(opts = {}) {
|
|
1306
|
-
const forceRefresh = opts.forceRefresh === true
|
|
1307
|
-
|| process.env.AGENTS_SESSIONS_FORCE_REFRESH === '1';
|
|
1308
|
-
// Scoped host lists are not represented in the unscoped fleet/local snapshot
|
|
1309
|
-
// — always gather live so a filter cannot return the wrong set.
|
|
1310
|
-
const scoped = (opts.hosts?.length ?? 0) > 0;
|
|
1311
|
-
if (opts.local && !scoped) {
|
|
1312
|
-
const loaded = await loadLocalActiveSessions({
|
|
1313
|
-
forceRefresh,
|
|
1314
|
-
gather: async () => {
|
|
1315
|
-
const rows = await getActiveSessions({ localOnly: true });
|
|
1316
|
-
const self = machineId();
|
|
1317
|
-
for (const s of rows)
|
|
1318
|
-
if (!s.machine)
|
|
1319
|
-
s.machine = self;
|
|
1320
|
-
return rows;
|
|
1321
|
-
},
|
|
1322
|
-
});
|
|
1323
|
-
return { sessions: loaded.sessions, remoteDeviceCount: 0 };
|
|
1324
|
-
}
|
|
1325
|
-
if (!opts.local && !scoped) {
|
|
1326
|
-
const loaded = await loadFleetActiveSessions({
|
|
1327
|
-
forceRefresh,
|
|
1328
|
-
gather: () => gatherActiveSessionsLive({ local: false }),
|
|
1329
|
-
});
|
|
1330
|
-
return {
|
|
1331
|
-
sessions: loaded.sessions,
|
|
1332
|
-
remoteDeviceCount: loaded.remoteDeviceCount,
|
|
1333
|
-
remoteSkipped: loaded.remoteSkipped,
|
|
1334
|
-
remoteDiscoveryFailed: loaded.remoteDiscoveryFailed,
|
|
1335
|
-
};
|
|
1336
|
-
}
|
|
1337
|
-
return gatherActiveSessionsLive(opts);
|
|
1338
|
-
}
|
|
1339
|
-
/**
|
|
1340
|
-
* Live (uncached) gather — the work the daemon / force-refresh path pays once
|
|
1341
|
-
* so other surfaces can share the snapshot. Kept separate so the cache layer
|
|
1342
|
-
* never recursively re-enters itself.
|
|
1343
|
-
*/
|
|
1344
|
-
async function gatherActiveSessionsLive(opts = {}) {
|
|
1345
|
-
const self = machineId();
|
|
1346
|
-
// An explicit --host/--device list scopes the view: seed local sessions only
|
|
1347
|
-
// when no hosts are named, or when this machine is one of the named targets.
|
|
1348
|
-
// `localOnly: opts.local` (RUSH-2118) keeps a `--local` query from dialing a
|
|
1349
|
-
// remote-host teammate over ssh even for this machine's OWN local gather —
|
|
1350
|
-
// "this machine only" must mean zero ssh, not just "skip the cross-machine
|
|
1351
|
-
// device fan-out below".
|
|
1352
|
-
const local = shouldIncludeLocal(opts.hosts, self)
|
|
1353
|
-
? await getActiveSessions({ localOnly: opts.local })
|
|
1354
|
-
: [];
|
|
1355
|
-
for (const s of local)
|
|
1356
|
-
if (!s.machine)
|
|
1357
|
-
s.machine = self;
|
|
1358
|
-
let remoteDeviceCount = 0;
|
|
1359
|
-
let remoteSkipped = [];
|
|
1360
|
-
let remoteDiscoveryFailed = false;
|
|
1361
|
-
let merged = local;
|
|
1362
|
-
if (!opts.local) {
|
|
1363
|
-
const remoteHosts = remoteHostsToDial(opts.hosts, self);
|
|
1364
|
-
// An explicit list naming only self leaves nothing remote to dial — skip the
|
|
1365
|
-
// fan-out rather than let an empty list fall through to the device sweep.
|
|
1366
|
-
if (!opts.hosts?.length || (remoteHosts && remoteHosts.length > 0)) {
|
|
1367
|
-
const remote = await gatherRemoteActive(remoteHosts);
|
|
1368
|
-
remoteDeviceCount = remote.deviceCount;
|
|
1369
|
-
remoteSkipped = remote.skipped;
|
|
1370
|
-
remoteDiscoveryFailed = remote.discoveryFailed;
|
|
1371
|
-
merged = dedupeByMachineSession([...local, ...remote.sessions]);
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
// Asking a box is not the same as a session running there: a host-dispatched
|
|
1375
|
-
// run is reported by its dispatcher while executing on the peer. Scope on the
|
|
1376
|
-
// machine the session runs on so `--device X` never lists someone else's work
|
|
1377
|
-
// (RUSH-2479). Applied here, in the single gather, so the interactive browser
|
|
1378
|
-
// gets the same answer as `--active --json`.
|
|
1379
|
-
return {
|
|
1380
|
-
sessions: filterActiveSessionsByHostScope(merged, opts.hosts, self),
|
|
1381
|
-
remoteDeviceCount,
|
|
1382
|
-
remoteSkipped,
|
|
1383
|
-
remoteDiscoveryFailed,
|
|
1384
|
-
};
|
|
1385
|
-
}
|
|
1386
|
-
/**
|
|
1387
|
-
* Explain an empty `--active` result instead of the old flat
|
|
1388
|
-
* "No active agent sessions." either way (RUSH-2507: a live fleet sweep found
|
|
1389
|
-
* ~41 running agents while this printed the same line as a genuinely idle
|
|
1390
|
-
* fleet). Re-probes local tmux health only now, on the empty path, and names
|
|
1391
|
-
* any remote peer that went unheard rather than silently folding it into
|
|
1392
|
-
* "nothing running".
|
|
1393
|
-
*/
|
|
1394
|
-
async function describeEmptyActiveDiscovery(opts, remoteSkipped, remoteDiscoveryFailed) {
|
|
1395
|
-
const parts = [];
|
|
1396
|
-
if (!opts.hosts?.length || opts.local) {
|
|
1397
|
-
const health = await describeActiveDiscoveryHealth();
|
|
1398
|
-
if (health.degradedSources.length > 0) {
|
|
1399
|
-
parts.push(`local discovery is degraded (${health.degradedSources.join(', ')} unreachable) — sessions may be hidden, run \`agents doctor\` to diagnose`);
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
if (remoteDiscoveryFailed) {
|
|
1403
|
-
parts.push('the device list could not be loaded — no peer was reachable to sweep');
|
|
1404
|
-
}
|
|
1405
|
-
else if (remoteSkipped && remoteSkipped.length > 0) {
|
|
1406
|
-
parts.push(`${remoteSkipped.length} peer(s) went unheard (${remoteSkipped.join(', ')}) — sessions there may be hidden`);
|
|
1407
|
-
}
|
|
1408
|
-
if (parts.length === 0)
|
|
1409
|
-
return 'No active agent sessions.';
|
|
1410
|
-
return `No active agent sessions found, but discovery was degraded: ${parts.join('; ')}.`;
|
|
1411
|
-
}
|
|
1412
|
-
/**
|
|
1413
|
-
* Render the unified active-session view, grouped by machine. Scoping and the
|
|
1414
|
-
* fleet sweep live in {@link gatherActiveSessions}; this owns the presentation
|
|
1415
|
-
* (the `--waiting` gate, JSON, and the grouped table). A tip is shown when there
|
|
1416
|
-
* are no other machines to include.
|
|
1417
|
-
*/
|
|
1418
|
-
async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
1419
|
-
const self = machineId();
|
|
1420
|
-
const gathered = await gatherActiveSessions(opts);
|
|
1421
|
-
const { remoteDeviceCount, remoteSkipped, remoteDiscoveryFailed } = gathered;
|
|
1422
|
-
// Backfill agent version, refs, created time, and routine provenance from the
|
|
1423
|
-
// historical index. Routine filtering needs that provenance before it narrows
|
|
1424
|
-
// the live pool; doing it here also enriches both JSON and human output.
|
|
1425
|
-
backfillActiveRowsFromIndex(gathered.sessions);
|
|
1426
|
-
// --bookmarks narrows the live view too. Applied HERE, not only in the
|
|
1427
|
-
// browser: the browser is skipped for --json, --waiting, a pipe, a multi-host
|
|
1428
|
-
// scope, and an SSH-fanout peer, and the flag silently did nothing on every
|
|
1429
|
-
// one of those paths — including `--active --bookmarks --json`, which is
|
|
1430
|
-
// exactly what the browser's own `y` copy-cmd hands to an agent.
|
|
1431
|
-
const merged = opts.bookmarksOnly
|
|
1432
|
-
? gathered.sessions.filter((s) => !!s.sessionId && listBookmarks().has(s.sessionId))
|
|
1433
|
-
: gathered.sessions;
|
|
1434
|
-
const routineFiltered = filterActiveSessionsByRoutine(merged, opts.routine);
|
|
1435
|
-
// Status flags form a union. --waiting additionally retains its scriptable
|
|
1436
|
-
// gate: exit non-zero when the union contains a session awaiting the user.
|
|
1437
|
-
const statusFiltered = opts.statuses?.length
|
|
1438
|
-
? routineFiltered.filter((session) => opts.statuses.some((status) => matchesLiveStatus(session, status)))
|
|
1439
|
-
: routineFiltered.filter(isRunningLiveSession);
|
|
1440
|
-
const sessions = statusFiltered;
|
|
1441
|
-
if (asJson) {
|
|
1442
|
-
// Resolve who is watching each local tmux pane before serializing: `viewingIn`
|
|
1443
|
-
// is how a consumer distinguishes a session someone is looking at from one
|
|
1444
|
-
// running orphaned after its terminal died. tmux-only (no osascript) so the
|
|
1445
|
-
// scriptable path stays cheap — see enrichTmuxLocators.
|
|
1446
|
-
await enrichTmuxLocators(sessions.filter(s => sessionProcessIsLocal(s, self)));
|
|
1447
|
-
process.stdout.write(JSON.stringify(serializeActiveSessionsForJson(sessions), null, 2) + '\n');
|
|
1448
|
-
if (waitingOnly && sessions.some(isAwaitingUser))
|
|
1449
|
-
process.exitCode = 1;
|
|
1450
|
-
return;
|
|
1451
|
-
}
|
|
1452
|
-
if (sessions.length === 0) {
|
|
1453
|
-
if (waitingOnly) {
|
|
1454
|
-
console.log(chalk.gray('No sessions waiting on input.'));
|
|
1455
|
-
return;
|
|
1456
|
-
}
|
|
1457
|
-
console.log(chalk.gray(await describeEmptyActiveDiscovery(opts, remoteSkipped, remoteDiscoveryFailed)));
|
|
1458
|
-
if (!opts.local && !opts.hosts?.length && remoteDeviceCount === 0)
|
|
1459
|
-
printCrossMachineTip();
|
|
1460
|
-
return;
|
|
1461
|
-
}
|
|
1462
|
-
// Enrich LOCAL sessions with jump locators (display-only, after the --json /
|
|
1463
|
-
// --waiting gates so scriptable output stays osascript-free). Remote sessions
|
|
1464
|
-
// keep their raw pane id — their tmux/Ghostty live on the other machine.
|
|
1465
|
-
await enrichLocalLocators(sessions.filter(s => sessionProcessIsLocal(s, self)));
|
|
1466
|
-
const grouped = groupSessionsByMachine(sessions, self);
|
|
1467
|
-
let firstMachine = true;
|
|
1468
|
-
for (const mg of grouped.machines) {
|
|
1469
|
-
if (!firstMachine)
|
|
1470
|
-
console.log();
|
|
1471
|
-
firstMachine = false;
|
|
1472
|
-
printMachineHeader(mg);
|
|
1473
|
-
renderWorkspaceLayout(mg.layout, ' ', mg.machine);
|
|
1474
|
-
}
|
|
1475
|
-
const parts = groupTally(sessions).split(' · ').filter(Boolean);
|
|
1476
|
-
// The synthetic "cloud" group is a category, not a machine — exclude it from the
|
|
1477
|
-
// machine count and note it separately so the tally stays truthful.
|
|
1478
|
-
const realMachines = grouped.machines.filter((m) => m.machine !== CLOUD_MACHINE_KEY).length;
|
|
1479
|
-
const hasCloud = grouped.machines.some((m) => m.machine === CLOUD_MACHINE_KEY);
|
|
1480
|
-
const machineWord = realMachines === 1 ? 'machine' : 'machines';
|
|
1481
|
-
const cloudNote = hasCloud ? ' + cloud' : '';
|
|
1482
|
-
console.log(chalk.gray(`\n${sessions.length} active (${parts.join(', ')}) across ${realMachines} ${machineWord}${cloudNote}.`));
|
|
1483
|
-
// Tip only when nothing else could be included and the user didn't opt out.
|
|
1484
|
-
if (!opts.local && !opts.hosts?.length && remoteDeviceCount === 0)
|
|
1485
|
-
printCrossMachineTip();
|
|
1486
|
-
// Scriptable gate: a non-zero exit when anything is waiting on the user.
|
|
1487
|
-
if (waitingOnly && sessions.some(isAwaitingUser))
|
|
1488
|
-
process.exitCode = 1;
|
|
1489
|
-
}
|
|
1490
|
-
/** Apply the routine selector to enriched live rows for every non-browser active view. */
|
|
1491
|
-
export function filterActiveSessionsByRoutine(sessions, routine) {
|
|
1492
|
-
if (!routine)
|
|
1493
|
-
return sessions;
|
|
1494
|
-
const routineSessions = sessions.filter((session) => session.origin === 'routine' || !!session.routineName);
|
|
1495
|
-
if (typeof routine !== 'string')
|
|
1496
|
-
return routineSessions;
|
|
1497
|
-
const names = [...new Set(routineSessions.map((session) => session.routineName).filter((name) => !!name))];
|
|
1498
|
-
const selected = resolveRoutineName(routine, names);
|
|
1499
|
-
return selected
|
|
1500
|
-
? routineSessions.filter((session) => session.routineName === selected)
|
|
1501
|
-
: [];
|
|
1502
|
-
}
|
|
1503
|
-
/** Match the status words users see, preserving activity's richer working signal. */
|
|
1504
|
-
export function matchesLiveStatus(session, status) {
|
|
1505
|
-
if (status === 'working')
|
|
1506
|
-
return session.activity === 'working' || (!session.activity && session.status === 'running');
|
|
1507
|
-
if (status === 'waiting')
|
|
1508
|
-
return isAwaitingUser(session);
|
|
1509
|
-
return session.status === status;
|
|
1510
|
-
}
|
|
1511
|
-
/** Resolve convenience flags once. Multiple flags intentionally form a union. */
|
|
1512
|
-
export function requestedLiveStatuses(options) {
|
|
1513
|
-
const statuses = [];
|
|
1514
|
-
if (options.working)
|
|
1515
|
-
statuses.push('working');
|
|
1516
|
-
if (options.idle)
|
|
1517
|
-
statuses.push('idle');
|
|
1518
|
-
if (options.waiting)
|
|
1519
|
-
statuses.push('waiting');
|
|
1520
|
-
if (options.orphan || options.orphaned)
|
|
1521
|
-
statuses.push('orphaned');
|
|
1522
|
-
if (options.crashed)
|
|
1523
|
-
statuses.push('crashed');
|
|
1524
|
-
if (options.closed)
|
|
1525
|
-
statuses.push('closed');
|
|
1526
|
-
if (options.abandoned)
|
|
1527
|
-
statuses.push('abandoned');
|
|
1528
|
-
if (options.queued)
|
|
1529
|
-
statuses.push('queued');
|
|
1530
|
-
if (options.unknown)
|
|
1531
|
-
statuses.push('unknown');
|
|
1532
|
-
return [...new Set(statuses)];
|
|
1533
|
-
}
|
|
1534
|
-
/** Nudge shown when `--active` has no other machines to fold in. */
|
|
1535
|
-
function printCrossMachineTip() {
|
|
1536
|
-
console.log(chalk.gray("\nTip: include sessions from your other machines — register them with 'ag devices sync', then rerun. Use --local to skip."));
|
|
1537
|
-
}
|
|
1538
|
-
/**
|
|
1539
|
-
* True when the interactive session browser should open instead of a printed
|
|
1540
|
-
* listing: a real TTY, no `--json`, and `--no-interactive` not set. The bare
|
|
1541
|
-
* listing and `--active` both default to it; scripts/pipes/agents fall through
|
|
1542
|
-
* to the existing printed/JSON paths.
|
|
1543
|
-
*/
|
|
1544
364
|
function useInteractiveBrowser(options) {
|
|
1545
365
|
return options.interactive !== false && !options.json && isInteractiveTerminal();
|
|
1546
366
|
}
|
|
1547
|
-
/**
|
|
1548
|
-
* A bare interactive fleet listing — no query, no render/filter flag — that the
|
|
1549
|
-
* `runSessionBrowser` picker can represent. The single predicate shared by the
|
|
1550
|
-
* bare-browser branch and the `--device` early-return guard so they can't drift:
|
|
1551
|
-
* when this holds, an explicit `--device` scope is folded into the
|
|
1552
|
-
* browser (preview-rich, selectable) instead of the legacy per-host raw stream.
|
|
1553
|
-
*/
|
|
1554
367
|
function isBareBrowserListing(options, query) {
|
|
1555
368
|
return (useInteractiveBrowser(options) &&
|
|
1556
|
-
// A peer answering a fan-out must never open a TUI. It has no TTY either, so
|
|
1557
|
-
// this is the explicit half of a guard that otherwise rests on the implicit
|
|
1558
|
-
// invariant that peers are always dialed with --json (see remote-list.ts).
|
|
1559
369
|
process.env.AGENTS_SESSIONS_LOCAL !== '1' &&
|
|
1560
370
|
hasNoBrowserDisqualifyingFlags(options, query));
|
|
1561
371
|
}
|
|
1562
|
-
/**
|
|
1563
|
-
* Pure flag-gate half of {@link isBareBrowserListing} (TTY-independent, so it is
|
|
1564
|
-
* unit-testable): true when no query, render, or filter flag is present that the
|
|
1565
|
-
* `runSessionBrowser` picker cannot represent.
|
|
1566
|
-
*/
|
|
1567
372
|
export function hasNoBrowserDisqualifyingFlags(options, query) {
|
|
1568
373
|
return (!query &&
|
|
1569
|
-
// A named team view is a flat selectable pool; the bare --teams report
|
|
1570
|
-
// stays grouped and printed because the browser cannot preserve its shape.
|
|
1571
374
|
(!options.teams || !!options.inTeam) &&
|
|
1572
375
|
!options.flat &&
|
|
1573
376
|
!options.tree &&
|
|
1574
377
|
!options.markdown &&
|
|
1575
378
|
!options.until &&
|
|
1576
379
|
!options.project &&
|
|
1577
|
-
// The interactive browser picker is a fuzzy-search TUI over the discovered
|
|
1578
|
-
// pool, not a SQL-filtered listing — it cannot represent a skill/plugin
|
|
1579
|
-
// scope. Falling through to it here would silently drop the filter and
|
|
1580
|
-
// show the unfiltered pool instead (same reasoning as --project/--sort).
|
|
1581
380
|
!options.skill &&
|
|
1582
381
|
!options.plugin &&
|
|
1583
382
|
!options.sort &&
|
|
1584
|
-
// --routine without a name owns a two-stage picker (routine -> run ->
|
|
1585
|
-
// session), and a named routine owns the run-grouped overview. The generic
|
|
1586
|
-
// browser can only flatten routine sessions into its ordinary row list, so
|
|
1587
|
-
// routing this flag there silently bypasses both routine-specific views.
|
|
1588
383
|
!options.routine &&
|
|
1589
384
|
!options.artifacts &&
|
|
1590
385
|
options.artifact === undefined &&
|
|
1591
|
-
// --cloud lists a provider's tasks, not the transcript index, and
|
|
1592
|
-
// runCloudSessions has no host scope — letting a `--device X --cloud` fall
|
|
1593
|
-
// through to the browser gate would silently drop the X the user asked for.
|
|
1594
386
|
!options.cloud &&
|
|
1595
|
-
// The browser carries ONE device in its filter and `y` copies back exactly
|
|
1596
|
-
// one --device, so a multi-host scope can't round-trip. Those stay on the
|
|
1597
|
-
// legacy per-host stream, which prints each peer under its own banner.
|
|
1598
387
|
(options.host?.length ?? 0) <= 1);
|
|
1599
388
|
}
|
|
1600
|
-
/** Resolve a typed routine selector by exact name, substring, or one unambiguous typo. */
|
|
1601
|
-
export function resolveRoutineName(query, names) {
|
|
1602
|
-
const normalized = query.trim().toLowerCase();
|
|
1603
|
-
if (!normalized)
|
|
1604
|
-
return null;
|
|
1605
|
-
const exact = names.find((name) => name.toLowerCase() === normalized);
|
|
1606
|
-
if (exact)
|
|
1607
|
-
return exact;
|
|
1608
|
-
const containing = names.filter((name) => name.toLowerCase().includes(normalized));
|
|
1609
|
-
if (containing.length === 1)
|
|
1610
|
-
return containing[0];
|
|
1611
|
-
return fuzzyMatch(query, names, FUZZY_PRESETS.dynamic);
|
|
1612
|
-
}
|
|
1613
389
|
export function buildRoutineRunGroups(sessions) {
|
|
1614
390
|
const byRun = new Map();
|
|
1615
391
|
for (const session of sessions) {
|
|
@@ -1641,10 +417,6 @@ export function buildRoutineChoices(sessions, runOnlyNames = []) {
|
|
|
1641
417
|
latestRunSessionCount: runs[0].sessions.length,
|
|
1642
418
|
};
|
|
1643
419
|
});
|
|
1644
|
-
// Routines whose only attempts are pre-session (blocked/skipped/failed-before-
|
|
1645
|
-
// spawn) have run records but no transcript. Surface them so the picker is
|
|
1646
|
-
// built from definitions+runs, never only transcripts — otherwise a routine
|
|
1647
|
-
// that has never produced a session reads as "No routines match" (the plan).
|
|
1648
420
|
const seen = new Set(choices.map((c) => c.name));
|
|
1649
421
|
for (const name of runOnlyNames) {
|
|
1650
422
|
if (seen.has(name))
|
|
@@ -1683,7 +455,6 @@ async function selectRoutineName(sessions) {
|
|
|
1683
455
|
});
|
|
1684
456
|
return picked?.item.name ?? null;
|
|
1685
457
|
}
|
|
1686
|
-
/** `listJobsWithRuns` but never throws into the sessions reader (best-effort). */
|
|
1687
458
|
function safeListJobsWithRuns() {
|
|
1688
459
|
try {
|
|
1689
460
|
return [...new Set([...listJobs().map((job) => job.name), ...listJobsWithRuns()])];
|
|
@@ -1692,17 +463,6 @@ function safeListJobsWithRuns() {
|
|
|
1692
463
|
return [];
|
|
1693
464
|
}
|
|
1694
465
|
}
|
|
1695
|
-
/**
|
|
1696
|
-
* Resolve which routine the user is targeting for `--routine`. Returns `null` to
|
|
1697
|
-
* ABORT the command (a bad `--routine <name>` printed an error, or the user
|
|
1698
|
-
* cancelled the interactive pick). `{ name: null }` means "no routine was
|
|
1699
|
-
* selected, don't filter" — the degenerate bare `--routine` piped to a
|
|
1700
|
-
* non-interactive sink. `{ name }` is a chosen routine.
|
|
1701
|
-
*
|
|
1702
|
-
* The name universe is transcripts UNION every routine with a definition or a
|
|
1703
|
-
* run record (`safeListJobsWithRuns`), so a command-only or never-ran routine —
|
|
1704
|
-
* which produces no `SessionMeta` — still resolves and drills down.
|
|
1705
|
-
*/
|
|
1706
466
|
async function selectRoutineTarget(sessions, routine, interactive) {
|
|
1707
467
|
const routineNames = [...new Set([
|
|
1708
468
|
...sessions.map((session) => session.routineName).filter((name) => !!name),
|
|
@@ -1742,10 +502,6 @@ export function executionKind(meta) {
|
|
|
1742
502
|
return 'agent';
|
|
1743
503
|
return 'command';
|
|
1744
504
|
}
|
|
1745
|
-
/**
|
|
1746
|
-
* Reconcile a routine's canonical run records (`listRuns`, local disk) with the
|
|
1747
|
-
* indexed sessions for that routine (fleet-wide, keyed by `routineRunId`).
|
|
1748
|
-
*/
|
|
1749
505
|
function buildRoutineDrilldown(name, sessions) {
|
|
1750
506
|
const runs = listRuns(name);
|
|
1751
507
|
const forRoutine = sessions.filter((s) => s.routineName === name);
|
|
@@ -1760,7 +516,6 @@ function buildRoutineDrilldown(name, sessions) {
|
|
|
1760
516
|
meta,
|
|
1761
517
|
sessions: (byRun.get(meta.runId) ?? []).sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)),
|
|
1762
518
|
}))
|
|
1763
|
-
// Newest run first: `startedAt`, then run id (an ISO-derived slot) as a tie-break.
|
|
1764
519
|
.sort((a, b) => {
|
|
1765
520
|
const at = a.meta.startedAt || a.meta.runId;
|
|
1766
521
|
const bt = b.meta.startedAt || b.meta.runId;
|
|
@@ -1773,13 +528,9 @@ function buildRoutineDrilldown(name, sessions) {
|
|
|
1773
528
|
orphanSessions,
|
|
1774
529
|
runRecordCount: runs.length,
|
|
1775
530
|
linkedSessionCount: forRoutine.length,
|
|
1776
|
-
// "Agent routine" = anything that isn't purely command runs. A workflow run
|
|
1777
|
-
// carries no `agent` field but still expects a session, so key off
|
|
1778
|
-
// executionKind, not `agent` alone.
|
|
1779
531
|
isAgentRoutine: runs.some((r) => executionKind(r) !== 'command') || forRoutine.length > 0,
|
|
1780
532
|
};
|
|
1781
533
|
}
|
|
1782
|
-
/** Glyph + word for a run's terminal status. */
|
|
1783
534
|
function runStatusCell(status) {
|
|
1784
535
|
switch (status) {
|
|
1785
536
|
case 'completed': return `${chalk.green('✓')} ${chalk.green('completed')}`;
|
|
@@ -1792,7 +543,6 @@ function runStatusCell(status) {
|
|
|
1792
543
|
default: return chalk.gray(status);
|
|
1793
544
|
}
|
|
1794
545
|
}
|
|
1795
|
-
/** Where a run's body executed: a host, a cloud provider, or this machine. */
|
|
1796
546
|
function runPlacement(meta) {
|
|
1797
547
|
if (meta.host)
|
|
1798
548
|
return `host:${meta.host}`;
|
|
@@ -1800,13 +550,11 @@ function runPlacement(meta) {
|
|
|
1800
550
|
return `cloud:${meta.cloudProvider}`;
|
|
1801
551
|
return 'local';
|
|
1802
552
|
}
|
|
1803
|
-
/** The trigger clause: kind + who triggered it (dropping the noisy UNRESOLVED). */
|
|
1804
553
|
function runTrigger(meta) {
|
|
1805
554
|
const kind = meta.triggerKind ?? 'schedule';
|
|
1806
555
|
const who = meta.triggeredBy && !meta.triggeredBy.startsWith('UNRESOLVED') ? ` by ${meta.triggeredBy}` : '';
|
|
1807
556
|
return `${kind}${who}`;
|
|
1808
557
|
}
|
|
1809
|
-
/** The tail clause explaining a run's outcome (exit code / error / skip reason). */
|
|
1810
558
|
function runOutcomeDetail(meta) {
|
|
1811
559
|
if (meta.status === 'skipped' && meta.skipReason) {
|
|
1812
560
|
const ref = meta.activeRunId ? ` → ${meta.activeRunId}` : '';
|
|
@@ -1819,13 +567,10 @@ function runOutcomeDetail(meta) {
|
|
|
1819
567
|
return 'daemon down at fire time';
|
|
1820
568
|
if (meta.errorMessage)
|
|
1821
569
|
return meta.errorMessage;
|
|
1822
|
-
// A clean `exit 0` is already implied by the green "completed" status — only a
|
|
1823
|
-
// non-zero code carries information worth a tail clause.
|
|
1824
570
|
if (typeof meta.exitCode === 'number' && meta.exitCode !== 0)
|
|
1825
571
|
return `exit ${meta.exitCode}`;
|
|
1826
572
|
return '';
|
|
1827
573
|
}
|
|
1828
|
-
/** Compact per-session metadata line under a linked session row. */
|
|
1829
574
|
function linkedSessionMeta(session) {
|
|
1830
575
|
const parts = [];
|
|
1831
576
|
const shown = sessionDisplayAgent(session);
|
|
@@ -1846,19 +591,11 @@ function linkedSessionMeta(session) {
|
|
|
1846
591
|
parts.push(`${session.toolCallCount} tools`);
|
|
1847
592
|
return chalk.gray(' ' + parts.join(' · '));
|
|
1848
593
|
}
|
|
1849
|
-
/**
|
|
1850
|
-
* Render the routine drilldown: canonical run history first, each run linked to
|
|
1851
|
-
* its indexed agent session(s). This is the run/session seam made visible — a
|
|
1852
|
-
* command routine shows its runs and states plainly that no session is expected;
|
|
1853
|
-
* an agent routine shows the same runs plus each run's session metadata.
|
|
1854
|
-
*/
|
|
1855
594
|
export function printRoutineDrilldown(drill, liveIndex, opts = {}) {
|
|
1856
595
|
const runWord = drill.runRecordCount === 1 ? 'run record' : 'run records';
|
|
1857
596
|
const sessWord = drill.linkedSessionCount === 1 ? 'linked session' : 'linked sessions';
|
|
1858
597
|
console.log(`${chalk.cyan.bold(drill.name)} ` +
|
|
1859
598
|
chalk.gray(`${drill.runRecordCount} ${runWord} · ${drill.linkedSessionCount} ${sessWord}`));
|
|
1860
|
-
// A routine with no session ever produced is a command routine — say so up
|
|
1861
|
-
// front so the empty session column reads as expected, not as missing data.
|
|
1862
599
|
if (!drill.isAgentRoutine) {
|
|
1863
600
|
console.log(chalk.gray('Command routine — runs execute a shell command; no agent session is produced.'));
|
|
1864
601
|
}
|
|
@@ -1881,9 +618,6 @@ export function printRoutineDrilldown(drill, liveIndex, opts = {}) {
|
|
|
1881
618
|
if (outcome)
|
|
1882
619
|
detailParts.push(outcome);
|
|
1883
620
|
console.log(chalk.gray(' ' + detailParts.join(' · ')));
|
|
1884
|
-
// Log/report access — both are children of the run dir that a pre-execution
|
|
1885
|
-
// terminal (blocked/skipped/missed) never wrote, so gate each on existence
|
|
1886
|
-
// rather than pointing at a file that isn't there.
|
|
1887
621
|
const runDir = getRunDir(drill.name, m.runId);
|
|
1888
622
|
const logHints = [];
|
|
1889
623
|
if (fs.existsSync(path.join(runDir, 'stdout.log')))
|
|
@@ -1899,8 +633,6 @@ export function printRoutineDrilldown(drill, liveIndex, opts = {}) {
|
|
|
1899
633
|
}
|
|
1900
634
|
}
|
|
1901
635
|
else if (kind !== 'command') {
|
|
1902
|
-
// An agent run that produced no session — say why (the run terminal
|
|
1903
|
-
// explains it), never fake a session row.
|
|
1904
636
|
console.log(chalk.gray(' no agent session archived for this run'));
|
|
1905
637
|
}
|
|
1906
638
|
console.log();
|
|
@@ -1918,16 +650,11 @@ export function printRoutineDrilldown(drill, liveIndex, opts = {}) {
|
|
|
1918
650
|
}
|
|
1919
651
|
}
|
|
1920
652
|
console.log(chalk.gray(`Run history from .history/runs/${drill.name}/ · resume a session with agents sessions resume <id>.`));
|
|
1921
|
-
// Every listing path must say what it dropped (sessions.ts invariant). Under
|
|
1922
|
-
// --routine team-origin sessions are shown (hiddenCount is 0), but unmanaged
|
|
1923
|
-
// installs may still be hidden — surface both footers when non-zero.
|
|
1924
653
|
if (opts.hiddenCount && opts.hiddenCount > 0)
|
|
1925
654
|
console.log(chalk.gray(formatTeamHiddenFooter(opts.hiddenCount)));
|
|
1926
655
|
if (opts.hiddenUnmanaged && opts.hiddenUnmanaged > 0)
|
|
1927
656
|
console.log(chalk.gray(formatUnmanagedHiddenFooter(opts.hiddenUnmanaged)));
|
|
1928
657
|
}
|
|
1929
|
-
/** The canonical `ag sessions …` command for a set of flags — the twin of the
|
|
1930
|
-
* browser's `y` hotkey (see --print-cmd). Normalizes to the stable flag form. */
|
|
1931
658
|
function canonicalSessionsCommand(query, options) {
|
|
1932
659
|
const a = ['sessions'];
|
|
1933
660
|
if (options.active)
|
|
@@ -1984,31 +711,8 @@ function canonicalSessionsCommand(query, options) {
|
|
|
1984
711
|
a.push(JSON.stringify(q));
|
|
1985
712
|
return 'ag ' + a.join(' ');
|
|
1986
713
|
}
|
|
1987
|
-
/** Bound on the `details.messages` array and on each message's `text` — this
|
|
1988
|
-
* rides a cached JSON payload (and, for the remote fast path, an SSH capture
|
|
1989
|
-
* with its own byte cap), so it is capped independently of either. */
|
|
1990
714
|
const SESSION_DETAIL_MAX_MESSAGES = 8;
|
|
1991
715
|
const SESSION_DETAIL_MESSAGE_MAX_CHARS = 4_000;
|
|
1992
|
-
/**
|
|
1993
|
-
* The additive `details` block on `sessions preview --json` (PHNX-3999):
|
|
1994
|
-
* `request`/`timeline`/`files` are read FIRST from the existing
|
|
1995
|
-
* `session_timelines` projection (`readSessionTimelineAny` — the daemon's own
|
|
1996
|
-
* bounded, incrementally-folded cache, `SessionTimelineProjection` in
|
|
1997
|
-
* `lib/session/db.ts`), so a session the daemon has already reached costs
|
|
1998
|
-
* nothing extra here. When that projection is missing (daemon hasn't reached
|
|
1999
|
-
* this session yet), this does an ON-DEMAND fold using the EXACT same pure,
|
|
2000
|
-
* already-redacting pipeline the daemon uses (`foldTimeline` /
|
|
2001
|
-
* `projectTimeline` / `projectSessionFiles`, `timeline.ts`) over whatever
|
|
2002
|
-
* events are cheaply available: the fresh-parse `events` from
|
|
2003
|
-
* `loadSessionPreviewDigest` when there is one, else a bounded 128 KiB tail
|
|
2004
|
-
* read (`readSessionTail`, Claude/Codex only — the same bounded reader the
|
|
2005
|
-
* local preview's over-cap fallback uses) so a WARM cache hit (no fresh parse)
|
|
2006
|
-
* still gets real, on-demand content instead of falling back to a 2-message
|
|
2007
|
-
* digest summary. This is genuinely bounded either way: it folds only events
|
|
2008
|
-
* already in hand or a fixed 128 KiB window, never a fresh whole-file parse.
|
|
2009
|
-
* `partial`/`reason` say when this happened (`daemon-pending: on-demand
|
|
2010
|
-
* bounded fold`) versus a session with no transcript at all.
|
|
2011
|
-
*/
|
|
2012
716
|
function sessionTranscriptStamp(session) {
|
|
2013
717
|
try {
|
|
2014
718
|
if (!session.filePath)
|
|
@@ -2025,13 +729,6 @@ export function buildSessionDetailBlock(session, digest, events, sourceStamp) {
|
|
|
2025
729
|
const stamp = sourceStamp ?? sessionTranscriptStamp(session);
|
|
2026
730
|
const canDateEvents = sourceStamp !== undefined || events.length === 0;
|
|
2027
731
|
const daemonProjection = stamp ? readSessionTimelineAny(session.id, stamp) : undefined;
|
|
2028
|
-
// A bounded tail read backs `messages` on EVERY warm cache hit (no fresh
|
|
2029
|
-
// parse this call), REGARDLESS of whether the daemon projection is already
|
|
2030
|
-
// present — this is what fixes a real bug review caught: a warm digest hit
|
|
2031
|
-
// whose daemon timeline HAD landed still fell through to the 2-message
|
|
2032
|
-
// digest summary below, because the tail-read used to be gated on the
|
|
2033
|
-
// on-demand-fold branch only. `messages` and the on-demand fold are two
|
|
2034
|
-
// independent uses of the same cheap, bounded events window.
|
|
2035
732
|
let foldEvents = events;
|
|
2036
733
|
if (foldEvents.length === 0 && session.filePath) {
|
|
2037
734
|
foldEvents = readSessionTail(session.filePath, session.agent);
|
|
@@ -2079,22 +776,7 @@ export function buildSessionDetailBlock(session, digest, events, sourceStamp) {
|
|
|
2079
776
|
reason,
|
|
2080
777
|
};
|
|
2081
778
|
}
|
|
2082
|
-
/** Resolve a session by id/query globally and print its compact preview (no pager).
|
|
2083
|
-
* Backs `--preview` — the fast path for the "peek before resume" hot loop. */
|
|
2084
779
|
export async function renderSessionPreview(query, scope) {
|
|
2085
|
-
// Exact ID + exactly one named, non-local device: the canonical bounded
|
|
2086
|
-
// preview loader (PHNX-3999) answers in ONE ssh hop plus a durable local
|
|
2087
|
-
// cache, instead of the general resolver's metadata fan-out followed by a
|
|
2088
|
-
// second render hop. Scoped tightly on purpose — a short id/label is not a
|
|
2089
|
-
// stable cache key across devices, and this never touches local discovery.
|
|
2090
|
-
//
|
|
2091
|
-
// Gated on `isCompleteSessionId`, NOT a bare-UUID regex: measured over this
|
|
2092
|
-
// fleet's own index, a "full session id" is a bare UUID for most harnesses
|
|
2093
|
-
// but `session_<uuid>` for kimi/rush and `ses_<ulid>` for opencode
|
|
2094
|
-
// (discover.ts). A bare-UUID-only check would silently skip this fast path
|
|
2095
|
-
// (falling through to the slower general resolver, never wrong, but wrong
|
|
2096
|
-
// to claim as "the fast path" for those harnesses) for a real, exact,
|
|
2097
|
-
// caller-supplied full id on those three.
|
|
2098
780
|
if (scope.json && !scope.local && scope.hosts?.length === 1
|
|
2099
781
|
&& scope.hosts[0] !== machineId() && isCompleteSessionId(query.trim())) {
|
|
2100
782
|
const { getRemoteSessionPreview } = await import('../lib/session/remote-preview-cache.js');
|
|
@@ -2106,37 +788,15 @@ export async function renderSessionPreview(query, scope) {
|
|
|
2106
788
|
console.log(JSON.stringify({
|
|
2107
789
|
schemaVersion: 1,
|
|
2108
790
|
session: envelope?.session ?? null,
|
|
2109
|
-
// `active` is a live snapshot the PEER computed at the moment it ran
|
|
2110
|
-
// `sessions preview --local --json` — meaningful only for that instant.
|
|
2111
|
-
// When this response is served from the durable cache (`cache.source ===
|
|
2112
|
-
// 'cache'`, which can be up to 45s old, or indefinitely old under a
|
|
2113
|
-
// matched --revision), presenting that stale snapshot under the same
|
|
2114
|
-
// `active` key would read as the session's CURRENT working/idle status
|
|
2115
|
-
// when it may no longer be. Null it out rather than relabel it with an
|
|
2116
|
-
// age stamp: the caller already has `cache.fetchedAt`/`cache.stale` to
|
|
2117
|
-
// reason about staleness, and getting a fresh `active` read costs
|
|
2118
|
-
// another SSH round trip this fast path exists specifically to avoid.
|
|
2119
791
|
active: result.cache.source === 'live' ? (envelope?.active ?? null) : null,
|
|
2120
792
|
preview: envelope?.preview ?? null,
|
|
2121
793
|
error: envelope?.error ?? (envelope ? null : result.cache.reason),
|
|
2122
|
-
// The peer's own `sessions preview --local --json` already computed
|
|
2123
|
-
// `details` (request/timeline/files/messages) with its own bounded reader
|
|
2124
|
-
// (see buildSessionDetailBlock) — this hop just forwards it verbatim,
|
|
2125
|
-
// never re-derives it, so a metadata-only/no-transcript peer row's
|
|
2126
|
-
// explicit unavailable reason survives the hop unchanged.
|
|
2127
794
|
details: envelope?.details ?? null,
|
|
2128
795
|
cache: result.cache,
|
|
2129
796
|
}));
|
|
2130
797
|
return;
|
|
2131
798
|
}
|
|
2132
799
|
let outcome = await resolveSessionMetadataValue(query, scope);
|
|
2133
|
-
// resolveSessionMetadataValue already consults the live registry for a running
|
|
2134
|
-
// session with no transcript row (RUSH-2682), so a live session resolves above.
|
|
2135
|
-
// This block still repairs the residual case — a session whose transcript is on
|
|
2136
|
-
// disk but not yet indexed (e.g. one that just ended). `waitForScan` makes the
|
|
2137
|
-
// repair honest: when another process holds the scan claim it waits for that
|
|
2138
|
-
// scan instead of returning the stale read. Also preserves Claude history-alias
|
|
2139
|
-
// discovery.
|
|
2140
800
|
if (outcome.kind !== 'resolved'
|
|
2141
801
|
&& (!scope.hosts?.length || shouldIncludeLocal(scope.hosts, machineId()))) {
|
|
2142
802
|
const discovered = applyScopeFilters(await discoverSessions({ all: true, cwd: process.cwd(), limit: 5000, waitForScan: true }), scope);
|
|
@@ -2151,10 +811,6 @@ export async function renderSessionPreview(query, scope) {
|
|
|
2151
811
|
}
|
|
2152
812
|
}
|
|
2153
813
|
if (outcome.kind === 'partial') {
|
|
2154
|
-
// RUSH-2492: an unreachable peer is a warning, not a hard failure. The
|
|
2155
|
-
// resolver already resolves an id found on the reachable fleet (SES-9a),
|
|
2156
|
-
// so reaching here means the session was not found on any device we COULD
|
|
2157
|
-
// reach — it may live on an unreachable peer, which we could not check.
|
|
2158
814
|
const offline = outcome.failedPeers;
|
|
2159
815
|
console.error(chalk.yellow(`Warning: ${offline.length} device(s) unreachable, not checked: ${offline.join(', ')}`));
|
|
2160
816
|
console.error(chalk.red(`No session matching "${query}" on any reachable device (${offline.length} unreachable, not checked).`));
|
|
@@ -2191,17 +847,12 @@ export async function renderSessionPreview(query, scope) {
|
|
|
2191
847
|
}
|
|
2192
848
|
return;
|
|
2193
849
|
}
|
|
2194
|
-
// Lead with the live status when the session is still running, so the preview
|
|
2195
|
-
// says working / waiting / idle up front — not just the historical transcript.
|
|
2196
|
-
// The shared snapshot is accepted for at most 15 seconds. The durable preview
|
|
2197
|
-
// below contains no live status, so a long-lived process can never keep a
|
|
2198
|
-
// stale working/waiting headline in its transcript cache.
|
|
2199
850
|
let live;
|
|
2200
851
|
try {
|
|
2201
852
|
const loaded = await loadLocalActiveSessions();
|
|
2202
853
|
live = indexActiveBySessionId(loaded.sessions).get(session.id);
|
|
2203
854
|
}
|
|
2204
|
-
catch {
|
|
855
|
+
catch { }
|
|
2205
856
|
if (scope.json) {
|
|
2206
857
|
const sourceStamp = sessionTranscriptStamp(session);
|
|
2207
858
|
const { digest, error, events } = loadSessionPreviewDigest(session);
|
|
@@ -2249,27 +900,12 @@ export async function renderSessionPreview(query, scope) {
|
|
|
2249
900
|
console.log(headline);
|
|
2250
901
|
console.log(buildPreview(session));
|
|
2251
902
|
}
|
|
2252
|
-
/**
|
|
2253
|
-
* The one-line live status banner shown above a session preview: the glyph, the
|
|
2254
|
-
* status word, and — when the session needs a human or has LOST one — a plain
|
|
2255
|
-
* sentence saying so. Shared by `--preview` and the interactive browser's preview
|
|
2256
|
-
* pane so both explain a state the same way.
|
|
2257
|
-
*
|
|
2258
|
-
* `crashed` and `orphaned` are the states a glyph alone cannot carry: nobody
|
|
2259
|
-
* reads "orphan" and knows it means "still running in tmux with no window
|
|
2260
|
-
* attached", so those two spell it out.
|
|
2261
|
-
*/
|
|
2262
903
|
export function formatLiveStatusHeadline(live, bookmarked = false) {
|
|
2263
904
|
const star = bookmarked ? chalk.yellow('★ ') : '';
|
|
2264
|
-
// With no live row there is no status to lead with, so the star has to say
|
|
2265
|
-
// what it means on its own — a bare `★` above a preview reads as noise.
|
|
2266
905
|
if (!live)
|
|
2267
906
|
return bookmarked ? chalk.yellow('★ bookmarked') : '';
|
|
2268
907
|
const { glyph } = liveGlyphAndPreview(live);
|
|
2269
908
|
const word = liveStatusWord(live) || live.status;
|
|
2270
|
-
// One definition, shared with `--waiting`. A second local copy drifted: the
|
|
2271
|
-
// preview said "needs you" for a dead session that `--waiting` had (rightly)
|
|
2272
|
-
// stopped counting, so the human and the script disagreed about the same row.
|
|
2273
909
|
const needsYou = isAwaitingUser(live);
|
|
2274
910
|
const reason = live.awaitingReason ? ` (${live.awaitingReason.replace('_', ' ')})` : '';
|
|
2275
911
|
let suffix = needsYou ? chalk.yellow(` ← needs you${reason}`) : '';
|
|
@@ -2277,16 +913,12 @@ export function formatLiveStatusHeadline(live, bookmarked = false) {
|
|
|
2277
913
|
suffix = chalk.redBright(' ← the host app or connection went away and took the agent with it');
|
|
2278
914
|
}
|
|
2279
915
|
else if (live.status === 'orphaned') {
|
|
2280
|
-
// Keep the needs-you half. A session sitting on a real question with nobody
|
|
2281
|
-
// attached is strictly worse than either fact alone, and replacing the
|
|
2282
|
-
// question with the generic orphan line undersells exactly that case.
|
|
2283
916
|
suffix = needsYou
|
|
2284
917
|
? chalk.yellow(` ← waiting on you${reason}, and no client is attached to answer it`)
|
|
2285
918
|
: chalk.yellow(' ← still running, but no client is attached — nothing is showing it');
|
|
2286
919
|
}
|
|
2287
920
|
return `${star}${glyph} ${statusColor(live.status)(word)}${suffix}`;
|
|
2288
921
|
}
|
|
2289
|
-
/** Merge local and peer envelopes without changing the versioned JSON shape. */
|
|
2290
922
|
export function mergeToolSearchEnvelopes(local, remotes) {
|
|
2291
923
|
const all = [local, ...remotes];
|
|
2292
924
|
const sessions = new Map();
|
|
@@ -2333,7 +965,6 @@ export function mergeToolProgramCountEnvelopes(local, remotes) {
|
|
|
2333
965
|
machines: all.flatMap((envelope) => envelope.machines),
|
|
2334
966
|
};
|
|
2335
967
|
}
|
|
2336
|
-
/** Partition a fleet query by transcript origin so synced mirrors cannot duplicate results. */
|
|
2337
968
|
export function toolOriginSessions(sessions, machine, originOnly) {
|
|
2338
969
|
return originOnly
|
|
2339
970
|
? sessions.filter((session) => (session.machine ?? machine) === machine)
|
|
@@ -2355,7 +986,6 @@ export function toolSearchFleetSortError(sort, spansDevices) {
|
|
|
2355
986
|
return undefined;
|
|
2356
987
|
return 'Tool search across devices supports only --sort recent; cost and duration are local-only.';
|
|
2357
988
|
}
|
|
2358
|
-
/** Compact grouped evidence for humans; JSON retains every bounded field. */
|
|
2359
989
|
function printToolSearch(envelope) {
|
|
2360
990
|
for (const session of envelope.sessions) {
|
|
2361
991
|
const machineName = session.machine
|
|
@@ -2394,14 +1024,7 @@ function printToolSearch(envelope) {
|
|
|
2394
1024
|
console.log(chalk.yellow(`Tool index coverage is partial: ${envelope.coverage.remainingFiles} transcript${envelope.coverage.remainingFiles === 1 ? '' : 's'} remain.${skipped}${limited}${retry}`));
|
|
2395
1025
|
}
|
|
2396
1026
|
}
|
|
2397
|
-
|
|
2398
|
-
async function sessionsAction(query, options,
|
|
2399
|
-
/**
|
|
2400
|
-
* Where commander got `--limit` from: 'cli'/'env' when the user supplied it,
|
|
2401
|
-
* 'default' when it filled in its own. Truthiness can't tell those apart,
|
|
2402
|
-
* because the default arrives as a string like any typed value.
|
|
2403
|
-
*/
|
|
2404
|
-
limitSource) {
|
|
1027
|
+
async function sessionsAction(query, options, limitSource) {
|
|
2405
1028
|
const queryClauses = options.query ?? [];
|
|
2406
1029
|
const liveStatuses = requestedLiveStatuses(options);
|
|
2407
1030
|
const liveOnly = options.active === true || liveStatuses.length > 0;
|
|
@@ -2418,8 +1041,6 @@ limitSource) {
|
|
|
2418
1041
|
process.exitCode = 1;
|
|
2419
1042
|
return;
|
|
2420
1043
|
}
|
|
2421
|
-
// Outside tool evidence mode, --query retains its original positional-search
|
|
2422
|
-
// meaning. The collector only makes it repeatable for tool clauses.
|
|
2423
1044
|
query = query ?? queryClauses[0];
|
|
2424
1045
|
}
|
|
2425
1046
|
if (options.fleet && !toolEvidenceMode) {
|
|
@@ -2468,9 +1089,6 @@ limitSource) {
|
|
|
2468
1089
|
return;
|
|
2469
1090
|
}
|
|
2470
1091
|
}
|
|
2471
|
-
// Normalize convenience flags before any routing reads them: per-agent
|
|
2472
|
-
// shorthands fold into --agent; --device and --devices (both
|
|
2473
|
-
// resolve against the same device registry).
|
|
2474
1092
|
applyAgentShorthands(options);
|
|
2475
1093
|
try {
|
|
2476
1094
|
query = applyVersionFilters(query, options);
|
|
@@ -2480,32 +1098,17 @@ limitSource) {
|
|
|
2480
1098
|
process.exitCode = 1;
|
|
2481
1099
|
return;
|
|
2482
1100
|
}
|
|
2483
|
-
// --device / --devices values are merged into options.host for internal routing. A bare `all` / `fleet` sentinel means
|
|
2484
|
-
// "search every peer" — which is already the default for `--active` and the
|
|
2485
|
-
// interactive listing — so it resolves to no explicit host set rather than
|
|
2486
|
-
// erroring on a device literally named "all".
|
|
2487
1101
|
const rawDeviceTargets = [...(options.device ?? []), ...(options.devices ?? [])];
|
|
2488
|
-
// But the HISTORICAL `--json` listing does NOT fan out by default (it stays a
|
|
2489
|
-
// deterministic local slice for scripts), so the sentinel would otherwise be
|
|
2490
|
-
// silently dropped and the fleet never reached (PHNX-2673). Remember it so that
|
|
2491
|
-
// path can sweep every peer, exactly as the interactive listing already does.
|
|
2492
1102
|
const fleetWide = rawDeviceTargets.some(t => t.toLowerCase() === 'all' || t.toLowerCase() === 'fleet');
|
|
2493
1103
|
const deviceTargets = rawDeviceTargets
|
|
2494
1104
|
.filter(t => t.toLowerCase() !== 'all' && t.toLowerCase() !== 'fleet');
|
|
2495
1105
|
if (deviceTargets.length > 0) {
|
|
2496
1106
|
options.host = [...(options.host ?? []), ...deviceTargets];
|
|
2497
1107
|
}
|
|
2498
|
-
// --print-cmd: echo the canonical `ag sessions …` for the given flags and exit.
|
|
2499
|
-
// The non-interactive twin of the browser's `y` hotkey — lets an agent compose
|
|
2500
|
-
// (or a human copy) the exact command a view maps to.
|
|
2501
1108
|
if (options.printCmd) {
|
|
2502
1109
|
process.stdout.write(canonicalSessionsCommand(query, options) + '\n');
|
|
2503
1110
|
return;
|
|
2504
1111
|
}
|
|
2505
|
-
// --roots: emit the local session-scan directories, per agent, as JSON. A pure
|
|
2506
|
-
// machine-readable query (no listing/render) — external watchers (the Factory
|
|
2507
|
-
// extension's fs.watch) read it to track the same dirs the CLI scans, instead
|
|
2508
|
-
// of hardcoding `~/.claude|.codex|.gemini`. Always local; ignores other flags.
|
|
2509
1112
|
if (options.roots) {
|
|
2510
1113
|
process.stdout.write(JSON.stringify(getSessionRoots(), null, 2) + '\n');
|
|
2511
1114
|
return;
|
|
@@ -2521,9 +1124,6 @@ limitSource) {
|
|
|
2521
1124
|
process.stdout.write(JSON.stringify({ launchId, sessionId: record?.session_id ?? null }) + '\n');
|
|
2522
1125
|
return;
|
|
2523
1126
|
}
|
|
2524
|
-
// --resolve is the metadata-only contract for downstream context workflows.
|
|
2525
|
-
// It reads indexed SessionMeta rows and never calls renderSession, buildPreview,
|
|
2526
|
-
// parseSession, or any other transcript renderer/parser.
|
|
2527
1127
|
if (options.resolve !== undefined || options.resolveSafeV1 !== undefined) {
|
|
2528
1128
|
if (options.resolveSafeV1 !== undefined && process.env[NO_FANOUT_ENV] !== '1') {
|
|
2529
1129
|
console.error(chalk.red('--resolve-safe-v1 is an internal fleet protocol.'));
|
|
@@ -2560,16 +1160,7 @@ limitSource) {
|
|
|
2560
1160
|
console.error(chalk.gray('Drop one — `--device <box>` to read that machine, `--local` to stay on this one.'));
|
|
2561
1161
|
process.exit(1);
|
|
2562
1162
|
}
|
|
2563
|
-
// --host WITHOUT --active. `--json` fans the recent listing out and emits ONE
|
|
2564
|
-
// clean merged SessionMeta[] array (same shape as the local --json path), for
|
|
2565
|
-
// scripts/extensions that JSON.parse a remote's history. Without --json it
|
|
2566
|
-
// keeps the legacy per-host stream (each remote's raw stdout under a
|
|
2567
|
-
// `── host ──` banner). With --active, the hosts are folded into the merged
|
|
2568
|
-
// machine-grouped view instead (handled below).
|
|
2569
1163
|
if (options.host && options.host.length > 0 && !liveOnly && !toolEvidenceMode) {
|
|
2570
|
-
// --local means "skip the SSH fan-out"; --host means "look only over there".
|
|
2571
|
-
// Together they ask for a peer's sessions without dialing the peer, which can
|
|
2572
|
-
// only ever be empty — so say that instead of rendering a blank list.
|
|
2573
1164
|
if (options.local === true && !shouldIncludeLocal(options.host, machineId())) {
|
|
2574
1165
|
console.error(chalk.red('--local and --device name opposite scopes: --local skips the SSH fan-out that --device needs.'));
|
|
2575
1166
|
console.error(chalk.gray('Drop one — `--device <box>` to read that machine, `--local` to stay on this one.'));
|
|
@@ -2579,11 +1170,6 @@ limitSource) {
|
|
|
2579
1170
|
await runRemoteSessionsJson(options.host);
|
|
2580
1171
|
return;
|
|
2581
1172
|
}
|
|
2582
|
-
// A bare interactive `--device`/`--device <box>` listing falls through to the
|
|
2583
|
-
// fleet browser below, which folds the named host(s) into the same merged,
|
|
2584
|
-
// preview-rich, selectable view as the local listing (via gatherRemoteList).
|
|
2585
|
-
// A query, a render/filter flag, or a non-interactive caller keeps the legacy
|
|
2586
|
-
// per-host raw stream under a `── host ──` banner.
|
|
2587
1173
|
if (!isBareBrowserListing(options, query)) {
|
|
2588
1174
|
try {
|
|
2589
1175
|
runRemoteSessions(options.host);
|
|
@@ -2595,9 +1181,6 @@ limitSource) {
|
|
|
2595
1181
|
return;
|
|
2596
1182
|
}
|
|
2597
1183
|
}
|
|
2598
|
-
// --preview <id/query>: resolve one session and print its compact preview, then
|
|
2599
|
-
// exit — checked before --active so `--active --preview <id>` peeks the id
|
|
2600
|
-
// rather than being swallowed by the active listing.
|
|
2601
1184
|
if (options.preview) {
|
|
2602
1185
|
if (!query) {
|
|
2603
1186
|
console.error(chalk.red('--preview requires a session id or query.'));
|
|
@@ -2607,63 +1190,18 @@ limitSource) {
|
|
|
2607
1190
|
return;
|
|
2608
1191
|
}
|
|
2609
1192
|
if (liveOnly) {
|
|
2610
|
-
// The running view is built from the live scan, which carries no team lineage
|
|
2611
|
-
// (that comes off the transcript index and the teams meta dir), so --in-team
|
|
2612
|
-
// has nothing to match on here. Say so rather than ignoring the flag.
|
|
2613
1193
|
if (options.inTeam) {
|
|
2614
1194
|
console.error(chalk.red('--in-team does not apply to --active: the running view carries no team lineage.'));
|
|
2615
1195
|
console.error(chalk.gray('Drop --active to filter by team, or use `agents teams status <name>` for a live team.'));
|
|
2616
1196
|
process.exit(1);
|
|
2617
1197
|
}
|
|
2618
|
-
|
|
2619
|
-
// running-only. --json / --waiting / --no-interactive / a peer fan-out keep the
|
|
2620
|
-
// static dump untouched, so scripts and agents are unaffected. An explicit
|
|
2621
|
-
// --since seeds the window; --until / --project (no browser field) or a
|
|
2622
|
-
// multi-host scope fall through to the static dump that already honors them.
|
|
2623
|
-
if (useInteractiveBrowser(options) &&
|
|
2624
|
-
liveStatuses.length === 0 &&
|
|
2625
|
-
!options.until &&
|
|
2626
|
-
!options.project &&
|
|
2627
|
-
!options.sort &&
|
|
2628
|
-
(options.host?.length ?? 0) <= 1 &&
|
|
2629
|
-
process.env.AGENTS_SESSIONS_LOCAL !== '1') {
|
|
2630
|
-
const { runSessionBrowser, activeBrowserSeed } = await import('./sessions-browser.js');
|
|
2631
|
-
await runSessionBrowser(activeBrowserSeed({
|
|
2632
|
-
teams: options.teams,
|
|
2633
|
-
agent: options.agent,
|
|
2634
|
-
host: options.host,
|
|
2635
|
-
since: options.since,
|
|
2636
|
-
all: options.all,
|
|
2637
|
-
bookmarks: options.bookmarks,
|
|
2638
|
-
routine: options.routine,
|
|
2639
|
-
}), { local: options.local === true, hosts: options.host });
|
|
2640
|
-
return;
|
|
2641
|
-
}
|
|
2642
|
-
// AGENTS_SESSIONS_LOCAL is set by a parent fan-out invocation (see
|
|
2643
|
-
// remote-active.ts) so a peer answers for itself without recursing.
|
|
2644
|
-
const forceLocal = options.local === true || process.env.AGENTS_SESSIONS_LOCAL === '1';
|
|
2645
|
-
await renderActiveSessions(options.json === true, options.waiting === true, {
|
|
2646
|
-
local: forceLocal,
|
|
2647
|
-
hosts: options.host,
|
|
2648
|
-
bookmarksOnly: options.bookmarks === true,
|
|
2649
|
-
statuses: liveStatuses,
|
|
2650
|
-
routine: options.routine,
|
|
2651
|
-
});
|
|
1198
|
+
await runLiveRoster(options);
|
|
2652
1199
|
return;
|
|
2653
1200
|
}
|
|
2654
1201
|
if (options.cloud) {
|
|
2655
1202
|
await runCloudSessions(query, options);
|
|
2656
1203
|
return;
|
|
2657
1204
|
}
|
|
2658
|
-
// Bare interactive listing → the interactive fleet browser (humans). A query,
|
|
2659
|
-
// a render/filter flag, --flat/--tree, --json, --until, --project (a named-project
|
|
2660
|
-
// filter the browser can't represent), or --no-interactive keep the existing
|
|
2661
|
-
// printed/render paths (agents and scripts unaffected). An explicit --since seeds
|
|
2662
|
-
// the browser's window so the flag is honored, not swallowed.
|
|
2663
|
-
//
|
|
2664
|
-
// Bare `--teams` still diverts to the printed team-grouped report because the
|
|
2665
|
-
// flat picker cannot preserve that shape. `--in-team <name> --teams` is one
|
|
2666
|
-
// flat lineage, so it qualifies through hasNoBrowserDisqualifyingFlags.
|
|
2667
1205
|
if (isBareBrowserListing(options, query)) {
|
|
2668
1206
|
const { runSessionBrowser, bareBrowserSeed } = await import('./sessions-browser.js');
|
|
2669
1207
|
await runSessionBrowser(bareBrowserSeed({
|
|
@@ -2687,7 +1225,6 @@ limitSource) {
|
|
|
2687
1225
|
process.exit(1);
|
|
2688
1226
|
}
|
|
2689
1227
|
const { agent, version } = parseAgentFilter(options.agent);
|
|
2690
|
-
// Path-like queries filter by project directory instead of text search.
|
|
2691
1228
|
let pathFilter;
|
|
2692
1229
|
let searchQuery;
|
|
2693
1230
|
if (query && looksLikePath(query)) {
|
|
@@ -2702,53 +1239,28 @@ limitSource) {
|
|
|
2702
1239
|
else {
|
|
2703
1240
|
searchQuery = query;
|
|
2704
1241
|
}
|
|
2705
|
-
// Artifact flags require a session query.
|
|
2706
1242
|
if ((options.artifacts || options.artifact !== undefined) && !query) {
|
|
2707
1243
|
console.error(chalk.red('--artifacts and --artifact require a session ID or query.'));
|
|
2708
1244
|
process.exit(1);
|
|
2709
1245
|
}
|
|
2710
1246
|
const mode = resolveViewMode(options, filterOpts);
|
|
2711
|
-
// --markdown or any filter flag forces single-session render.
|
|
2712
1247
|
const wantsRender = !toolEvidenceMode && (mode === 'markdown' || hasAnyFilter(filterOpts));
|
|
2713
|
-
// Artifact-list or artifact-read paths: widen scope and resolve session globally.
|
|
2714
1248
|
if ((options.artifacts || options.artifact !== undefined) && searchQuery) {
|
|
2715
1249
|
await renderArtifactsGlobal(searchQuery, options.artifacts ?? false, options.artifact, artifactLookupScope(options.agent, options.project, options.routine));
|
|
2716
1250
|
return;
|
|
2717
1251
|
}
|
|
2718
|
-
// A supplied ID is already the lookup key. Resolve it directly from SQLite
|
|
2719
|
-
// and, when needed, the peer indexes before any broad transcript discovery.
|
|
2720
1252
|
if (!toolEvidenceMode && searchQuery && looksLikeSessionId(searchQuery)) {
|
|
2721
1253
|
await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, routine: options.routine, filter: filterOpts, redact: options.redact, local: options.local, hosts: options.host });
|
|
2722
1254
|
return;
|
|
2723
1255
|
}
|
|
2724
|
-
// When the user explicitly asks to render (via mode flag), resolve the
|
|
2725
|
-
// query globally so sessions outside the default cwd/30d window are found.
|
|
2726
1256
|
if (wantsRender && searchQuery) {
|
|
2727
1257
|
await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, routine: options.routine, filter: filterOpts, redact: options.redact, local: options.local, hosts: options.host });
|
|
2728
1258
|
return;
|
|
2729
1259
|
}
|
|
2730
|
-
// Interactive picker loads a deep pool but shows only recent sessions
|
|
2731
|
-
// until the user starts typing. Non-interactive/JSON uses the explicit limit.
|
|
2732
1260
|
const isInteractive = !options.json && isInteractiveTerminal();
|
|
2733
|
-
// The grouped project overview is the default for a bare interactive listing:
|
|
2734
|
-
// no query, no path drill-in, not explicitly --flat/--tree. It drops the silent
|
|
2735
|
-
// cwd-scope + 50-cap + 30-day window that hide most of a large index.
|
|
2736
1261
|
const wantsOverview = isInteractive && !searchQuery && !pathFilter && !options.flat && !options.tree;
|
|
2737
|
-
// --in-team asks for ONE team's whole lineage, which is a handful of rows that
|
|
2738
|
-
// can sit anywhere in history. Bounding it by the default top-50 / 30-day window
|
|
2739
|
-
// silently returns nothing for any team older than that — so the flag widens its
|
|
2740
|
-
// own scope, the way --all does, unless the caller set an explicit --limit.
|
|
2741
1262
|
const wantsWholeTeam = !!options.inTeam;
|
|
2742
|
-
// A routine is not tied to the shell's cwd: its archived sessions retain the
|
|
2743
|
-
// working directory in which the scheduled agent actually ran. Scope the
|
|
2744
|
-
// routine catalog across directories so invoking it from ~/.agents/.system,
|
|
2745
|
-
// a project repo, or $HOME yields the same routine history.
|
|
2746
1263
|
const wantsWholeRoutine = !!options.routine;
|
|
2747
|
-
// `--limit` has a commander default, so an untouched flag still arrives as a
|
|
2748
|
-
// string and truthiness can't tell it from a typed one. Commander records where
|
|
2749
|
-
// each value came from, which is the only signal that distinguishes an explicit
|
|
2750
|
-
// `--limit 50` from no flag at all — a script asking for 50 must get 50, not the
|
|
2751
|
-
// whole-team pool.
|
|
2752
1264
|
const userSetLimit = limitSource === 'cli' || limitSource === 'env';
|
|
2753
1265
|
const limit = wantsOverview
|
|
2754
1266
|
? OVERVIEW_POOL_LIMIT
|
|
@@ -2758,8 +1270,6 @@ limitSource) {
|
|
|
2758
1270
|
process.exitCode = 1;
|
|
2759
1271
|
return;
|
|
2760
1272
|
}
|
|
2761
|
-
// Overview: recency order across the whole index, no default window; an explicit
|
|
2762
|
-
// --since still narrows. Non-overview keeps the prior interactive-30d default.
|
|
2763
1273
|
const since = wantsOverview
|
|
2764
1274
|
? options.since
|
|
2765
1275
|
: (options.since ?? (isInteractive && !options.all && !wantsWholeTeam && !wantsWholeRoutine ? '30d' : undefined));
|
|
@@ -2774,23 +1284,12 @@ limitSource) {
|
|
|
2774
1284
|
const spinner = options.json ? null : ora().start();
|
|
2775
1285
|
const tracker = createScanProgressTracker(LOAD_VERBS, 'sessions', spinner);
|
|
2776
1286
|
try {
|
|
2777
|
-
// Team-origin filter is pushed down to SQL so the LIMIT applies AFTER it.
|
|
2778
|
-
// Without this, a dev dir with heavy SDK spawn activity (Task subagents,
|
|
2779
|
-
// `agents run`, team agents) can fill the top-N window entirely with
|
|
2780
|
-
// hidden rows and make real CLI sessions appear to vanish.
|
|
2781
|
-
// 'recent' is the user-facing alias for the default timestamp sort.
|
|
2782
1287
|
const sortBy = options.sort === 'cost' ? 'cost' : options.sort === 'duration' ? 'duration' : 'timestamp';
|
|
2783
1288
|
const scope = {
|
|
2784
1289
|
agent,
|
|
2785
1290
|
version,
|
|
2786
|
-
// --in-team spans directories by construction: a team's teammates run in
|
|
2787
|
-
// their own worktrees, so scoping to the current cwd hides most of the
|
|
2788
|
-
// lineage the flag exists to show.
|
|
2789
1291
|
all: pathFilter ? undefined : options.all || wantsWholeTeam || wantsWholeRoutine || toolSpansDevices,
|
|
2790
1292
|
cwd: process.cwd(),
|
|
2791
|
-
// Default overview scopes to the current repo SUBTREE (prefix match), so a
|
|
2792
|
-
// monorepo shows its sub-projects grouped instead of collapsing to the one
|
|
2793
|
-
// exact-cwd project. `--all` clears the prefix and spans the whole index.
|
|
2794
1293
|
cwdPrefix: pathFilter ?? (wantsOverview && !options.all && !wantsWholeTeam && !wantsWholeRoutine && !toolSpansDevices ? process.cwd() : undefined),
|
|
2795
1294
|
project: options.project,
|
|
2796
1295
|
since,
|
|
@@ -2835,22 +1334,10 @@ limitSource) {
|
|
|
2835
1334
|
}
|
|
2836
1335
|
tracker.stop();
|
|
2837
1336
|
spinner?.stop();
|
|
2838
|
-
// Version filter is pushed down to SQL via scope.version above; no
|
|
2839
|
-
// post-filter needed. Defensive: the team-origin SQL filter covers the
|
|
2840
|
-
// ~100% case, but classifyTeamSession also recognizes sessions with a
|
|
2841
|
-
// meta.json in ~/.agents/teams/agents whose is_team_origin flag was
|
|
2842
|
-
// never set (legacy rows). Keep the in-memory pass so those are still
|
|
2843
|
-
// enriched/hidden.
|
|
2844
1337
|
const { visible: visibleSessions } = filterTeamSessions(sessions, shouldShowTeamSessions(options));
|
|
2845
1338
|
sessions = visibleSessions;
|
|
2846
|
-
// --in-team spans both ends of the lineage, so it can't be one SQL predicate:
|
|
2847
|
-
// the orchestrator matches on the scan-derived `spawnedTeam` column, while a
|
|
2848
|
-
// teammate only knows its team from the meta.json filterTeamSessions just
|
|
2849
|
-
// read. Match either, after that pass has populated `teamOrigin`.
|
|
2850
1339
|
if (options.inTeam)
|
|
2851
1340
|
sessions = sessions.filter((s) => matchesTeam(s, options.inTeam));
|
|
2852
|
-
// --bookmarks narrows to the bookmarked set. Applied here, before the JSON
|
|
2853
|
-
// emit, so `--bookmarks --json` is the machine-readable twin of the `b` key.
|
|
2854
1341
|
if (options.bookmarks) {
|
|
2855
1342
|
const bookmarks = listBookmarks();
|
|
2856
1343
|
sessions = sessions.filter((s) => bookmarks.has(s.id));
|
|
@@ -2911,28 +1398,15 @@ limitSource) {
|
|
|
2911
1398
|
}
|
|
2912
1399
|
return;
|
|
2913
1400
|
}
|
|
2914
|
-
// Under --in-team the visible list is one team, so the whole-index team-origin
|
|
2915
|
-
// count would be a non-sequitur next to it.
|
|
2916
1401
|
const hiddenCount = shouldShowTeamSessions(options) || options.inTeam
|
|
2917
1402
|
? 0
|
|
2918
1403
|
: countSessionsInScope({ ...scope, onlyTeamOrigin: true });
|
|
2919
|
-
// A typed routine scope must apply before smart ID routing. Otherwise an ID
|
|
2920
|
-
// from another routine resolves and renders before the routine filter below.
|
|
2921
1404
|
if (typeof options.routine === 'string') {
|
|
2922
1405
|
const filteredByRoutine = await filterSessionsByRoutine(sessions, options.routine, false);
|
|
2923
1406
|
if (!filteredByRoutine)
|
|
2924
1407
|
return;
|
|
2925
1408
|
sessions = filteredByRoutine;
|
|
2926
1409
|
}
|
|
2927
|
-
// Smart ID routing: a bare query that resolves to one session renders
|
|
2928
|
-
// directly. If nothing matches in the scoped window and the query looks
|
|
2929
|
-
// like a session ID, widen to global scope (incl. Claude /resume history).
|
|
2930
|
-
//
|
|
2931
|
-
// Exception: a PEER answering a parent's `--json` locate sweep
|
|
2932
|
-
// (AGENTS_SESSIONS_LOCAL=1) must return the SessionMeta[] ROW for the id, not
|
|
2933
|
-
// render its transcript — the sweep parses an array (parseRemoteList). So when
|
|
2934
|
-
// we are that peer and --json is set, skip the single-session short-circuit and
|
|
2935
|
-
// fall through to the array-emitting --json block below.
|
|
2936
1410
|
const answeringJsonSweep = options.json === true && process.env[NO_FANOUT_ENV] === '1';
|
|
2937
1411
|
if (searchQuery && !answeringJsonSweep) {
|
|
2938
1412
|
const idMatches = resolveSessionById(sessions, searchQuery);
|
|
@@ -2952,25 +1426,11 @@ limitSource) {
|
|
|
2952
1426
|
return;
|
|
2953
1427
|
sessions = filteredByRoutine;
|
|
2954
1428
|
}
|
|
2955
|
-
// An id-shaped query resolves by id ONLY — the same rule the render path
|
|
2956
|
-
// uses (resolveSessionQuery). Without this a `--json <uuid>` (as issued by
|
|
2957
|
-
// the fleet locate sweep, which runs `sessions <uuid> --json --local` on
|
|
2958
|
-
// each peer) would fall to FTS content search and return every transcript
|
|
2959
|
-
// that merely MENTIONS the id, defeating exact remote resolution. A genuine
|
|
2960
|
-
// search phrase keeps the ranked metadata+content path.
|
|
2961
1429
|
let filtered = searchQuery
|
|
2962
1430
|
? resolveSessionQuery(sessions, searchQuery, {
|
|
2963
1431
|
scope: { agent: options.agent, project: options.project, routine: options.routine },
|
|
2964
1432
|
}).matches
|
|
2965
1433
|
: sessions;
|
|
2966
|
-
// `--device all`/`--fleet` on a historical `--json` query fans out to every
|
|
2967
|
-
// peer and merges their rows in — the SAME SSH sweep the interactive listing
|
|
2968
|
-
// below uses, and the whole-fleet twin of the explicit `--device <host>
|
|
2969
|
-
// --json` path (`runRemoteSessionsJson`). Without this the documented
|
|
2970
|
-
// "search the whole fleet" flag returned local rows only (PHNX-2673). Guarded
|
|
2971
|
-
// to the sentinel so a bare `--json` stays a deterministic local slice for
|
|
2972
|
-
// scripts, and skipped under `--local`/the peer recursion guard. Dead peers
|
|
2973
|
-
// are skipped by the fan-out, never fatal — enrichment, not a dependency.
|
|
2974
1434
|
const forceLocalJson = options.local === true || process.env[NO_FANOUT_ENV] === '1';
|
|
2975
1435
|
if (fleetWide && !forceLocalJson) {
|
|
2976
1436
|
const forwarded = ensureWholeIndex(buildForwardedArgs(process.argv, new Set(options.host ?? [])));
|
|
@@ -2984,15 +1444,11 @@ limitSource) {
|
|
|
2984
1444
|
}
|
|
2985
1445
|
}
|
|
2986
1446
|
catch {
|
|
2987
|
-
// fan-out is an enrichment, never a hard dependency
|
|
2988
1447
|
}
|
|
2989
1448
|
finally {
|
|
2990
1449
|
fanSpinner?.stop();
|
|
2991
1450
|
}
|
|
2992
1451
|
}
|
|
2993
|
-
// JSON is the canonical picker contract: enrich the durable rows once
|
|
2994
|
-
// from the shared live cache so every consumer gets lifecycle/recovery
|
|
2995
|
-
// metadata without performing its own join or transcript scan.
|
|
2996
1452
|
const live = await gatherActiveSessions({
|
|
2997
1453
|
local: forceLocalJson,
|
|
2998
1454
|
hosts: options.host,
|
|
@@ -3000,19 +1456,8 @@ limitSource) {
|
|
|
3000
1456
|
process.stdout.write(serializeSessionsJson(serializeSessionPickerRows(filtered, live.sessions)));
|
|
3001
1457
|
return;
|
|
3002
1458
|
}
|
|
3003
|
-
// Cross-machine fan-out: unless --local (or we ARE a peer answering a
|
|
3004
|
-
// parent's sweep), fold in other online machines' sessions live over SSH so
|
|
3005
|
-
// the list spans the fleet without any sync — each remote row carries the
|
|
3006
|
-
// machine it came from, and the picker/table label + group by it. Only the
|
|
3007
|
-
// interactive picker and the printed table get this; --json and single-id
|
|
3008
|
-
// resolution above stay local (a peer answers for itself; scripts get a
|
|
3009
|
-
// deterministic local slice). Best-effort: a fan-out failure leaves the
|
|
3010
|
-
// local list intact rather than erroring the whole command.
|
|
3011
1459
|
const forceLocal = options.local === true || process.env[NO_FANOUT_ENV] === '1';
|
|
3012
1460
|
if (!forceLocal) {
|
|
3013
|
-
// Pass the hosts set so a variadic `--host a b` never leaks a host as a
|
|
3014
|
-
// query (defensive: the --host-without-active early return above already
|
|
3015
|
-
// means we only get here in auto-discovery mode, with no --host in argv).
|
|
3016
1461
|
const forwarded = buildForwardedArgs(process.argv, new Set(options.host ?? []));
|
|
3017
1462
|
if (!forwarded.includes('--json'))
|
|
3018
1463
|
forwarded.push('--json');
|
|
@@ -3024,7 +1469,6 @@ limitSource) {
|
|
|
3024
1469
|
}
|
|
3025
1470
|
}
|
|
3026
1471
|
catch {
|
|
3027
|
-
// fan-out is an enrichment, never a hard dependency
|
|
3028
1472
|
}
|
|
3029
1473
|
finally {
|
|
3030
1474
|
fanSpinner?.stop();
|
|
@@ -3033,32 +1477,21 @@ limitSource) {
|
|
|
3033
1477
|
if (options.routine) {
|
|
3034
1478
|
const target = await selectRoutineTarget(sessions, options.routine, isInteractive);
|
|
3035
1479
|
if (!target)
|
|
3036
|
-
return;
|
|
1480
|
+
return;
|
|
3037
1481
|
if (target.name) {
|
|
3038
1482
|
sessions = sessions.filter((s) => s.routineName === target.name);
|
|
3039
|
-
// A routine browse renders the run-first drilldown — canonical run history
|
|
3040
|
-
// plus each run's linked session — instead of dead-ending a
|
|
3041
|
-
// command/pre-session routine into the generic empty copy. An explicit
|
|
3042
|
-
// --flat/--tree or a search query keeps the scoped session listing/picker,
|
|
3043
|
-
// so `--routine x --flat` prints the plain table and `--routine x <id>`
|
|
3044
|
-
// resolves a specific session within the routine.
|
|
3045
1483
|
if (!searchQuery && !options.flat && !options.tree) {
|
|
3046
1484
|
const liveIndex = await maybeLiveIndex(options);
|
|
3047
1485
|
printRoutineDrilldown(buildRoutineDrilldown(target.name, sessions), liveIndex, { hiddenCount, hiddenUnmanaged });
|
|
3048
1486
|
return;
|
|
3049
1487
|
}
|
|
3050
1488
|
}
|
|
3051
|
-
// target.name === null: bare `--routine` piped to a non-interactive sink —
|
|
3052
|
-
// no routine chosen, fall through to the normal listing unfiltered.
|
|
3053
1489
|
}
|
|
3054
1490
|
if (sessions.length === 0) {
|
|
3055
1491
|
if (pathFilter) {
|
|
3056
1492
|
console.log(chalk.gray(`No sessions found for ${pathFilter}.`));
|
|
3057
1493
|
}
|
|
3058
1494
|
else if (options.routine) {
|
|
3059
|
-
// A routine scoped to a flat/tree/query listing with no matching session
|
|
3060
|
-
// (e.g. a command routine has none) — say so in routine terms, not the
|
|
3061
|
-
// generic cwd/--all copy, and point at the run-history drilldown.
|
|
3062
1495
|
console.log(chalk.gray('No indexed agent sessions for this routine. Drop --flat/--tree to see its run history.'));
|
|
3063
1496
|
}
|
|
3064
1497
|
else {
|
|
@@ -3072,27 +1505,16 @@ limitSource) {
|
|
|
3072
1505
|
}
|
|
3073
1506
|
return;
|
|
3074
1507
|
}
|
|
3075
|
-
// `--teams` prints the team-grouped report: teammates under their team (with
|
|
3076
|
-
// spawner + spawn time), bare SDK spawns in their own bucket. --flat/--tree
|
|
3077
|
-
// keep their inline table rendering; a search query keeps the picker. Placed
|
|
3078
|
-
// before the project overview and picker so it wins the bare `--teams` case.
|
|
3079
1508
|
if (options.teams && !options.flat && !options.tree && !searchQuery && !pathFilter) {
|
|
3080
1509
|
const liveIndex = await maybeLiveIndex(options);
|
|
3081
1510
|
printTeamsView(sessions, liveIndex, hiddenUnmanaged);
|
|
3082
1511
|
return;
|
|
3083
1512
|
}
|
|
3084
|
-
// The grouped project overview is the bare interactive default: a scannable
|
|
3085
|
-
// dashboard of the whole fleet grouped by project, newest-active first.
|
|
3086
|
-
// Interact/resume via `agents sessions <project>` or `agents sessions resume`.
|
|
3087
1513
|
if (wantsOverview) {
|
|
3088
1514
|
const liveIndex = await maybeLiveIndex(options);
|
|
3089
|
-
// Per-project row cap is fixed (--limit carries a default of 50 and drives
|
|
3090
|
-
// the fetch pool, not the display); `--all` expands every group instead.
|
|
3091
1515
|
printSessionOverview(sessions, hiddenCount, liveIndex, { perProjectCap: OVERVIEW_ROWS_PER_PROJECT, expand: !!options.all, hiddenUnmanaged });
|
|
3092
1516
|
return;
|
|
3093
1517
|
}
|
|
3094
|
-
// --tree / --flat are printed listings, not an interactive pick — render them
|
|
3095
|
-
// directly even in a TTY. A search query keeps the interactive picker.
|
|
3096
1518
|
if (isInteractiveTerminal() && !options.tree && !options.flat) {
|
|
3097
1519
|
const message = pathFilter
|
|
3098
1520
|
? `Search sessions (${path.basename(pathFilter)}):`
|
|
@@ -3104,7 +1526,6 @@ limitSource) {
|
|
|
3104
1526
|
}
|
|
3105
1527
|
return;
|
|
3106
1528
|
}
|
|
3107
|
-
// Non-interactive fallback (piped output) or --flat/--tree.
|
|
3108
1529
|
const filtered = searchQuery
|
|
3109
1530
|
? filterSessionsByQuery(sessions, searchQuery, {
|
|
3110
1531
|
agent: options.agent,
|
|
@@ -3114,8 +1535,6 @@ limitSource) {
|
|
|
3114
1535
|
: sessions;
|
|
3115
1536
|
const liveIndex = await maybeLiveIndex(options);
|
|
3116
1537
|
printSessionTable(filtered, hiddenCount, options.tree === true, liveIndex);
|
|
3117
|
-
// Every listing path must say what it dropped — a hidden default that stays
|
|
3118
|
-
// silent in one render mode is the failure this footer exists to prevent.
|
|
3119
1538
|
if (hiddenUnmanaged > 0)
|
|
3120
1539
|
console.log(chalk.gray(formatUnmanagedHiddenFooter(hiddenUnmanaged)));
|
|
3121
1540
|
}
|
|
@@ -3126,13 +1545,6 @@ limitSource) {
|
|
|
3126
1545
|
process.exit(1);
|
|
3127
1546
|
}
|
|
3128
1547
|
}
|
|
3129
|
-
/**
|
|
3130
|
-
* Prefix marking a row as somebody's teammate: `[<team>/<handle>] `, falling back
|
|
3131
|
-
* to the handle alone when the record predates team-name capture. The mode is
|
|
3132
|
-
* deliberately dropped here (it survives in the preview pane) — this tag is
|
|
3133
|
-
* folded into the topic cell, whose floor is 16 columns, so every character it
|
|
3134
|
-
* takes is one the actual prompt loses.
|
|
3135
|
-
*/
|
|
3136
1548
|
function teamTag(session) {
|
|
3137
1549
|
const origin = session.teamOrigin;
|
|
3138
1550
|
if (!origin)
|
|
@@ -3143,32 +1555,14 @@ function teamTag(session) {
|
|
|
3143
1555
|
return `[${team}${handle ? `/${handle}` : ''}] `;
|
|
3144
1556
|
return handle ? `[${handle}] ` : '[team] ';
|
|
3145
1557
|
}
|
|
3146
|
-
/**
|
|
3147
|
-
* Whether a session belongs to `team`, from either end: it spawned the team, or
|
|
3148
|
-
* it is one of the team's teammates. Case-insensitive, matching the SQL
|
|
3149
|
-
* predicate behind `querySessions({ spawnedTeam })`.
|
|
3150
|
-
*/
|
|
3151
1558
|
export function matchesTeam(session, team) {
|
|
3152
|
-
// The needle is peer-derived in the browser: `f.team` comes off the team cycle,
|
|
3153
|
-
// which is built from rows another machine sent. Guard it the same way as the
|
|
3154
|
-
// fields it is compared against, so a non-string can't throw out of a filter
|
|
3155
|
-
// that runs over every row.
|
|
3156
1559
|
const want = safeTeamText(team)?.trim().toLowerCase();
|
|
3157
1560
|
if (!want)
|
|
3158
1561
|
return true;
|
|
3159
1562
|
return (safeTeamText(session.spawnedTeam)?.toLowerCase() === want ||
|
|
3160
1563
|
safeTeamText(session.teamOrigin?.team)?.toLowerCase() === want);
|
|
3161
1564
|
}
|
|
3162
|
-
/** Longest team name rendered in the `team:` row badge before truncation. */
|
|
3163
1565
|
const TEAM_BADGE_MAX = 10;
|
|
3164
|
-
/**
|
|
3165
|
-
* The `team:<name>` badge for a session that SPAWNED a team — the orchestrator
|
|
3166
|
-
* end of the lineage, from the scan-derived `spawnedTeam`. Returned as a plain
|
|
3167
|
-
* (uncolored) string plus its display width so callers can reserve the width
|
|
3168
|
-
* from the topic budget and color it as their own segment: folding it into the
|
|
3169
|
-
* topic string would lose the color, since renderTopicCell strips ANSI and
|
|
3170
|
-
* re-whitens every slice.
|
|
3171
|
-
*/
|
|
3172
1566
|
export function teamBadge(session) {
|
|
3173
1567
|
const team = safeTeamText(session.spawnedTeam);
|
|
3174
1568
|
if (!team)
|
|
@@ -3181,7 +1575,6 @@ function originTag(session) {
|
|
|
3181
1575
|
return '';
|
|
3182
1576
|
return `[routine${session.routineName ? ` · ${session.routineName}` : ''}] `;
|
|
3183
1577
|
}
|
|
3184
|
-
/** Adapt a SessionMeta's persisted signals to the badge renderer's shape. */
|
|
3185
1578
|
function metaSignals(s) {
|
|
3186
1579
|
return {
|
|
3187
1580
|
pr: s.prUrl ? { url: s.prUrl, number: s.prNumber } : undefined,
|
|
@@ -3189,23 +1582,7 @@ function metaSignals(s) {
|
|
|
3189
1582
|
ticket: s.ticketId ? { id: s.ticketId } : undefined,
|
|
3190
1583
|
};
|
|
3191
1584
|
}
|
|
3192
|
-
/**
|
|
3193
|
-
* Narrowest topic a row is willing to render. The time cell drops its creation
|
|
3194
|
-
* field rather than squeeze the topic past this — a row that wraps is worse than
|
|
3195
|
-
* a row missing one field.
|
|
3196
|
-
*/
|
|
3197
1585
|
const MIN_TOPIC_W = 16;
|
|
3198
|
-
/**
|
|
3199
|
-
* The trailing time cell, as two fields: `3d → 2 hours ago` — when the session
|
|
3200
|
-
* was created, then when it was last active. One label answers neither "is this
|
|
3201
|
-
* the old session I'm looking for" nor "how long did it run", since a session
|
|
3202
|
-
* touched an hour ago may have started last week.
|
|
3203
|
-
*
|
|
3204
|
-
* Collapses to last-activity alone when the session ran under a minute (both
|
|
3205
|
-
* halves would name the same moment) or when `topicSlack` — the columns the row
|
|
3206
|
-
* has left for its topic — cannot spare the extra width. `extraW` is what the
|
|
3207
|
-
* creation field cost, for the caller to take off the topic budget.
|
|
3208
|
-
*/
|
|
3209
1586
|
function timeCell(age, topicSlack) {
|
|
3210
1587
|
const lastOnly = { plain: age.last, text: chalk.gray(age.last), extraW: 0 };
|
|
3211
1588
|
if (!age.created)
|
|
@@ -3216,12 +1593,6 @@ function timeCell(age, topicSlack) {
|
|
|
3216
1593
|
return lastOnly;
|
|
3217
1594
|
return { plain: prefix + age.last, text: chalk.dim(prefix) + chalk.gray(age.last), extraW };
|
|
3218
1595
|
}
|
|
3219
|
-
/** One flat table row:
|
|
3220
|
-
* shortId · agent · version · model · project · [glyph] label·doing · [ticket] · [wt] · time
|
|
3221
|
-
* `doing` is the live preview when running, else the topic. The `ticket` column
|
|
3222
|
-
* (tracker/PR ref, pulled out of the badge blob so refs align) is only rendered
|
|
3223
|
-
* when `showTicket` — otherwise a listing with no refs would waste a column of
|
|
3224
|
-
* dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
|
|
3225
1596
|
export function flatSessionRow(session, live, showTicket = false, cols = {}, bookmarked = false) {
|
|
3226
1597
|
const shown = sessionDisplayAgent(session);
|
|
3227
1598
|
const agentColor = colorAgent(shown);
|
|
@@ -3230,18 +1601,12 @@ export function flatSessionRow(session, live, showTicket = false, cols = {}, boo
|
|
|
3230
1601
|
const tag = originTag(session) || teamTag(session);
|
|
3231
1602
|
const label = session.label;
|
|
3232
1603
|
const { glyph, preview } = liveGlyphAndPreview(live);
|
|
3233
|
-
// A running session's live preview (via liveGlyphAndPreview → buildSessionDescription)
|
|
3234
|
-
// already folds in ActiveSession.todos. For resting / not-live rows, surface
|
|
3235
|
-
// SessionMeta.todos when the scan attached it.
|
|
3236
1604
|
const restingTodo = !live ? formatTodoCompact(session.todos) : '';
|
|
3237
1605
|
const topicBase = tag ? `${tag}${session.topic ?? ''}` : session.topic;
|
|
3238
1606
|
const doing = [restingTodo, preview || topicBase].filter(Boolean).join(' · ') || undefined;
|
|
3239
1607
|
const wt = session.worktreeSlug ? chalk.magenta(`wt:${session.worktreeSlug}`) : '';
|
|
3240
1608
|
const team = teamBadge(session);
|
|
3241
1609
|
const teamSeg = team.plain ? chalk.green(team.plain) : '';
|
|
3242
|
-
// The machine column only earns its width when the listing spans more than one
|
|
3243
|
-
// box (i.e. the cross-machine fan-out folded remotes in) — same rule and
|
|
3244
|
-
// pool-derived width as the picker.
|
|
3245
1610
|
const machineColW = cols.machineWidth ?? PICKER_MACHINE_W;
|
|
3246
1611
|
const machineCell = cols.showMachine
|
|
3247
1612
|
? chalk.gray(padToWidth(truncateToWidth((cols.machineLabel?.(session.machine ?? '') ?? session.machine ?? '') || '-', machineColW - 1), machineColW))
|
|
@@ -3250,8 +1615,6 @@ export function flatSessionRow(session, live, showTicket = false, cols = {}, boo
|
|
|
3250
1615
|
const ticketCell = showTicket
|
|
3251
1616
|
? chalk.blue(linkTicketCell(session, padToWidth(truncateToWidth(ticketLabel(session) || '-', TICKET_W), TICKET_W + 1)))
|
|
3252
1617
|
: '';
|
|
3253
|
-
// Live status word (working / waiting / idle) next to the glyph — the default
|
|
3254
|
-
// list is no longer a bare glyph. Empty (zero width) for resting rows.
|
|
3255
1618
|
const { cell: statusCell, width: statusW } = liveStatusCell(live);
|
|
3256
1619
|
const glyphW = glyph ? 2 : 0;
|
|
3257
1620
|
const machineW = cols.showMachine ? machineColW : 0;
|
|
@@ -3259,11 +1622,8 @@ export function flatSessionRow(session, live, showTicket = false, cols = {}, boo
|
|
|
3259
1622
|
const wtW = wt ? stringWidth(wt) + 1 : 0;
|
|
3260
1623
|
const width = terminalWidth();
|
|
3261
1624
|
const requestedModelW = cols.showModel ? (cols.modelWidth ?? PICKER_MODEL_MAX) : 0;
|
|
3262
|
-
// Same conditional 2 cells as the picker's marker, for the same reason.
|
|
3263
1625
|
const bookmarkW = cols.showBookmark ? 2 : 0;
|
|
3264
1626
|
const bookmarkCell = cols.showBookmark ? (bookmarked ? chalk.yellow('★ ') : ' ') : '';
|
|
3265
|
-
// Sized against the last-activity label alone, so the creation field is an
|
|
3266
|
-
// additive decision the row makes only once it knows what space is left.
|
|
3267
1627
|
const fixedW = bookmarkW + (10 + 9 + 8 + 16) + glyphW + statusW + machineW + ticketW + wtW + team.width + stringWidth(age.last) + 1;
|
|
3268
1628
|
const modelSlack = width - fixedW - MIN_TOPIC_W;
|
|
3269
1629
|
const modelW = requestedModelW <= modelSlack
|
|
@@ -3286,7 +1646,6 @@ export function flatSessionRow(session, live, showTicket = false, cols = {}, boo
|
|
|
3286
1646
|
(wt ? wt + ' ' : '') +
|
|
3287
1647
|
when.text);
|
|
3288
1648
|
}
|
|
3289
|
-
/** One tree-mode row (grouped under a dir header): id · agent · badges · topic · time. No version/project column. */
|
|
3290
1649
|
function treeSessionRow(session, live) {
|
|
3291
1650
|
const shown = sessionDisplayAgent(session);
|
|
3292
1651
|
const agentColor = colorAgent(shown);
|
|
@@ -3294,8 +1653,6 @@ function treeSessionRow(session, live) {
|
|
|
3294
1653
|
const tag = originTag(session) || teamTag(session);
|
|
3295
1654
|
const label = session.label;
|
|
3296
1655
|
const { glyph, preview } = liveGlyphAndPreview(live);
|
|
3297
|
-
// Match flatSessionRow: live preview already folds ActiveSession.todos; resting
|
|
3298
|
-
// rows surface SessionMeta.todos when present (RUSH-2045).
|
|
3299
1656
|
const restingTodo = !live ? formatTodoCompact(session.todos) : '';
|
|
3300
1657
|
const topicBase = preview || (tag ? `${tag}${session.topic ?? ''}` : session.topic);
|
|
3301
1658
|
const topic = [restingTodo, topicBase].filter(Boolean).join(' · ') || '-';
|
|
@@ -3319,38 +1676,16 @@ function treeSessionRow(session, live) {
|
|
|
3319
1676
|
padToWidth(chalk.white(truncateToWidth(head, topicW)), topicW) +
|
|
3320
1677
|
' ' + when.text);
|
|
3321
1678
|
}
|
|
3322
|
-
/**
|
|
3323
|
-
* Live-session index for enriching the default listing, or undefined when
|
|
3324
|
-
* enrichment is off (`--no-live`) or irrelevant (`--json`, which serializes
|
|
3325
|
-
* SessionMeta). Full detection (incl. the headless `ps` scan) is deliberate:
|
|
3326
|
-
* bare-CLI and tmux agents are the common case here, and skipping them would
|
|
3327
|
-
* leave the glyph almost never showing. The listing is a one-shot user action,
|
|
3328
|
-
* not a hot loop, so the `ps`/`lsof` cost is acceptable; `--no-live` is the
|
|
3329
|
-
* escape hatch. Never throws — a probe failure just yields a plain listing.
|
|
3330
|
-
*/
|
|
3331
1679
|
export async function maybeLiveIndex(options) {
|
|
3332
1680
|
if (options.live === false || options.json)
|
|
3333
1681
|
return undefined;
|
|
3334
1682
|
try {
|
|
3335
|
-
// `--local` promises "this machine only" for the default listing too (see
|
|
3336
|
-
// the `--local` help text), so it must gate the live-enrichment probe the
|
|
3337
|
-
// same way `--active --local` does — never dial a remote-host teammate
|
|
3338
|
-
// over ssh here either (RUSH-2118).
|
|
3339
1683
|
return indexActiveBySessionId(await getActiveSessions({ localOnly: options.local === true }));
|
|
3340
1684
|
}
|
|
3341
1685
|
catch {
|
|
3342
1686
|
return undefined;
|
|
3343
1687
|
}
|
|
3344
1688
|
}
|
|
3345
|
-
/**
|
|
3346
|
-
* Group key for the overview: resolve the cwd through the same canonical
|
|
3347
|
-
* resolver the `agents feed` timeline groups by — a defined project's name
|
|
3348
|
-
* when `defs` contains the cwd (multi-repo projects read as one group), else
|
|
3349
|
-
* the repo-level key, so a monorepo subdir (`<repo>/apps/cli`) reads as
|
|
3350
|
-
* `<repo>` in both views. Falls back to the indexed project name (stamped at
|
|
3351
|
-
* scan time) when the cwd carries nothing usable, e.g. a remote path this
|
|
3352
|
-
* machine cannot see still folds by basename through the same resolver.
|
|
3353
|
-
*/
|
|
3354
1689
|
export function overviewProjectKey(s, defs) {
|
|
3355
1690
|
const resolved = defs?.length ? resolveProjectNameForCwd(s.cwd, defs) : resolveProjectKey(s.cwd);
|
|
3356
1691
|
if (resolved)
|
|
@@ -3359,12 +1694,6 @@ export function overviewProjectKey(s, defs) {
|
|
|
3359
1694
|
return s.project.trim();
|
|
3360
1695
|
return '(no project)';
|
|
3361
1696
|
}
|
|
3362
|
-
/**
|
|
3363
|
-
* Turn a recency-descending pool into project groups: each group shows its
|
|
3364
|
-
* `perProjectCap` most-recent sessions (the rest become `· N more`), and groups
|
|
3365
|
-
* are ordered by their most-recent session so the newest-active project leads.
|
|
3366
|
-
* `perProjectCap = Infinity` expands every group. Pure — unit-tested.
|
|
3367
|
-
*/
|
|
3368
1697
|
export function buildOverviewGroups(pool, perProjectCap, defs) {
|
|
3369
1698
|
const byKey = new Map();
|
|
3370
1699
|
for (const s of pool) {
|
|
@@ -3374,17 +1703,12 @@ export function buildOverviewGroups(pool, perProjectCap, defs) {
|
|
|
3374
1703
|
const cap = Math.max(1, perProjectCap);
|
|
3375
1704
|
const groups = [];
|
|
3376
1705
|
for (const [key, rows] of byKey) {
|
|
3377
|
-
const shown = rows.slice(0, cap);
|
|
1706
|
+
const shown = rows.slice(0, cap);
|
|
3378
1707
|
groups.push({ key, total: rows.length, shown, more: rows.length - shown.length, maxTs: rows[0].lastActivity ?? rows[0].timestamp });
|
|
3379
1708
|
}
|
|
3380
1709
|
groups.sort((a, b) => (a.maxTs < b.maxTs ? 1 : a.maxTs > b.maxTs ? -1 : a.key.localeCompare(b.key)));
|
|
3381
1710
|
return { groups, projectCount: byKey.size };
|
|
3382
1711
|
}
|
|
3383
|
-
/**
|
|
3384
|
-
* The grouped project overview — the bare interactive default. Shows the latest
|
|
3385
|
-
* sessions grouped under their project, newest-active project first, with a
|
|
3386
|
-
* `· N more` per project and a `+N more projects` when the list is capped.
|
|
3387
|
-
*/
|
|
3388
1712
|
function printSessionOverview(pool, hiddenCount, liveIndex, opts) {
|
|
3389
1713
|
const { groups } = buildOverviewGroups(pool, opts.expand ? Infinity : opts.perProjectCap, listProjectDefs());
|
|
3390
1714
|
const shownGroups = opts.expand ? groups : groups.slice(0, OVERVIEW_MAX_PROJECTS);
|
|
@@ -3419,8 +1743,6 @@ function printSessionOverview(pool, hiddenCount, liveIndex, opts) {
|
|
|
3419
1743
|
}
|
|
3420
1744
|
function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
3421
1745
|
if (tree) {
|
|
3422
|
-
// Group by directory; drop the id/version columns from view. The short id
|
|
3423
|
-
// stays as each row's leading handle (the address to read/resume it).
|
|
3424
1746
|
const byDir = new Map();
|
|
3425
1747
|
for (const s of sessions) {
|
|
3426
1748
|
const key = s.cwd || s.project || 'unknown';
|
|
@@ -3448,9 +1770,6 @@ function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
|
3448
1770
|
console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
|
|
3449
1771
|
return;
|
|
3450
1772
|
}
|
|
3451
|
-
// Only show the ticket column when at least one row carries a ref — otherwise
|
|
3452
|
-
// it's a column of dashes that steals width from every topic. The machine
|
|
3453
|
-
// column (and its compact labels) is computed the same way the picker does it.
|
|
3454
1773
|
const showTicket = sessions.some((s) => ticketLabel(s) !== '');
|
|
3455
1774
|
const cols = pickerColumnsFor(sessions);
|
|
3456
1775
|
const bookmarks = listBookmarks();
|
|
@@ -3463,17 +1782,8 @@ function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
|
3463
1782
|
console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
|
|
3464
1783
|
}
|
|
3465
1784
|
}
|
|
3466
|
-
/** Width of the mode cell (plan/edit/auto/skip) in a team-member row. */
|
|
3467
1785
|
const TEAM_MODE_W = 5;
|
|
3468
|
-
/** Longest teammate handle folded into a team-member row before it truncates. */
|
|
3469
1786
|
const TEAM_HANDLE_W = 16;
|
|
3470
|
-
/**
|
|
3471
|
-
* One row under a team group: `shortId · agent · mode · handle · doing · time`.
|
|
3472
|
-
* Unlike {@link flatSessionRow}/{@link treeSessionRow} it drops the
|
|
3473
|
-
* `[team/handle]` topic prefix — the group header already names the team — and
|
|
3474
|
-
* promotes the teammate's mode and handle to their own cells, the richer
|
|
3475
|
-
* identity the `--teams` view exists to show (RUSH-1997).
|
|
3476
|
-
*/
|
|
3477
1787
|
function teamMemberRow(session, live) {
|
|
3478
1788
|
const shown = sessionDisplayAgent(session);
|
|
3479
1789
|
const agentColor = colorAgent(shown);
|
|
@@ -3482,8 +1792,6 @@ function teamMemberRow(session, live) {
|
|
|
3482
1792
|
const handle = safeTeamText(origin?.handle) ?? session.shortId;
|
|
3483
1793
|
const mode = safeTeamText(origin?.mode) ?? '';
|
|
3484
1794
|
const { glyph, preview } = liveGlyphAndPreview(live);
|
|
3485
|
-
// Match flatSessionRow: a live preview already folds ActiveSession.todos; a
|
|
3486
|
-
// resting row surfaces SessionMeta.todos when the scan attached it.
|
|
3487
1795
|
const restingTodo = !live ? formatTodoCompact(session.todos) : '';
|
|
3488
1796
|
const doing = [restingTodo, preview || session.topic || ''].filter(Boolean).join(' · ');
|
|
3489
1797
|
const shownHandle = truncateToWidth(handle, TEAM_HANDLE_W);
|
|
@@ -3506,14 +1814,9 @@ function teamMemberRow(session, live) {
|
|
|
3506
1814
|
' ' +
|
|
3507
1815
|
when.text);
|
|
3508
1816
|
}
|
|
3509
|
-
/** The header line for one team group (or the residual no-team bucket). */
|
|
3510
1817
|
function teamGroupHeader(g, glyph, labelById) {
|
|
3511
1818
|
const count = chalk.gray(`(${g.sessions.length})`);
|
|
3512
1819
|
if (g.kind === 'noTeam') {
|
|
3513
|
-
// These carry the isTeamOrigin flag but no teammate meta.json — a Task
|
|
3514
|
-
// sub-agent's transcript is never indexed, so in practice this bucket is
|
|
3515
|
-
// headless `agents run` spawns or teammates whose meta aged out. Named
|
|
3516
|
-
// honestly rather than claimed as real teammates.
|
|
3517
1820
|
return (chalk.magenta('▸') + ' ' + chalk.magenta.bold(NO_TEAM_GROUP_KEY) + ' ' + count +
|
|
3518
1821
|
' ' + chalk.gray('team-flagged spawns with no team record (`agents run`, or aged-out teammates)'));
|
|
3519
1822
|
}
|
|
@@ -3527,14 +1830,6 @@ function teamGroupHeader(g, glyph, labelById) {
|
|
|
3527
1830
|
bits.push(chalk.gray(`spawned ${formatRelativeTime(g.firstSpawnTs)}`));
|
|
3528
1831
|
return bits.join(' ');
|
|
3529
1832
|
}
|
|
3530
|
-
/**
|
|
3531
|
-
* The `agents sessions --teams` view: team-origin sessions grouped by team, the
|
|
3532
|
-
* richer display RUSH-1997 asks for. Each named team names its spawner (the
|
|
3533
|
-
* orchestrator session that created it) and spawn time, with its teammates'
|
|
3534
|
-
* mode + handle under it; team-flagged spawns that carry no teammate record fall
|
|
3535
|
-
* into a trailing no-team bucket, so a real teammate and a bare spawn are never
|
|
3536
|
-
* conflated. A printed report like `--tree`, not an interactive pick.
|
|
3537
|
-
*/
|
|
3538
1833
|
function printTeamsView(pool, liveIndex, hiddenUnmanaged = 0) {
|
|
3539
1834
|
const groups = groupSessionsByTeam(pool);
|
|
3540
1835
|
if (groups.length === 0) {
|
|
@@ -3542,9 +1837,6 @@ function printTeamsView(pool, liveIndex, hiddenUnmanaged = 0) {
|
|
|
3542
1837
|
console.log(chalk.gray('Team sessions are spawned by `agents teams` — see `agents teams status`.'));
|
|
3543
1838
|
return;
|
|
3544
1839
|
}
|
|
3545
|
-
// Resolve a spawner session id to its human label from the pool. The
|
|
3546
|
-
// orchestrator may or may not be in view; fall back to the short id, the same
|
|
3547
|
-
// degradation the --active teams rows use (active.ts resolveOrchestratorLabels).
|
|
3548
1840
|
const labelById = new Map();
|
|
3549
1841
|
for (const s of pool) {
|
|
3550
1842
|
const label = sessionHeadline(s);
|
|
@@ -3604,11 +1896,6 @@ function buildFilterOptions(options) {
|
|
|
3604
1896
|
function hasAnyFilter(opts) {
|
|
3605
1897
|
return !!(opts.include?.length || opts.exclude?.length || opts.first !== undefined || opts.last !== undefined);
|
|
3606
1898
|
}
|
|
3607
|
-
/**
|
|
3608
|
-
* Default is summary. Any explicit format flag wins. When filters are present
|
|
3609
|
-
* without a format, default to markdown since summary is an aggregate view
|
|
3610
|
-
* that filters don't meaningfully narrow.
|
|
3611
|
-
*/
|
|
3612
1899
|
function resolveViewMode(options, filters) {
|
|
3613
1900
|
if (options.markdown)
|
|
3614
1901
|
return 'markdown';
|
|
@@ -3618,35 +1905,13 @@ function resolveViewMode(options, filters) {
|
|
|
3618
1905
|
return 'markdown';
|
|
3619
1906
|
return 'summary';
|
|
3620
1907
|
}
|
|
3621
|
-
/**
|
|
3622
|
-
* Render a resolved session to stdout — the non-follow view behind
|
|
3623
|
-
* `agents logs <sessionId>`. Defaults to the concise `summary` digest (same as
|
|
3624
|
-
* `agents sessions <id>`); pass `'markdown'` for the full transcript
|
|
3625
|
-
* (`agents logs <id> --full`). Reuses the shared `renderSession` renderer.
|
|
3626
|
-
*/
|
|
3627
1908
|
export async function renderSessionLog(session, mode = 'summary') {
|
|
3628
1909
|
await renderSession(session, mode, {});
|
|
3629
1910
|
}
|
|
3630
|
-
/**
|
|
3631
|
-
* Emit one session exactly as `agents sessions <id> --json` does — the same
|
|
3632
|
-
* redact-by-default `{ session, events }` shape — so `agents logs <id> --json`
|
|
3633
|
-
* shares one machine-readable session contract instead of inventing another.
|
|
3634
|
-
*/
|
|
3635
1911
|
export async function renderSessionLogJson(session) {
|
|
3636
1912
|
await renderSession(session, 'json', {});
|
|
3637
1913
|
}
|
|
3638
|
-
/**
|
|
3639
|
-
* Render a file-gone (archived) session from the local DB (RUSH-2436). The
|
|
3640
|
-
* transcript file is deleted, but the user turns survive in session_text and the
|
|
3641
|
-
* last-computed digest in session_preview_cache — serve those so `agents sessions
|
|
3642
|
-
* <id>` still shows the prompts instead of an empty metadata dump. User-turn text
|
|
3643
|
-
* is the durable content every harness stores; assistant text survives only as
|
|
3644
|
-
* the cached digest's `lastAssistant` snippet, shown when present.
|
|
3645
|
-
*/
|
|
3646
1914
|
function renderArchivedSession(session, mode, options = {}) {
|
|
3647
|
-
// Redact by default, mirroring the live render path (render.ts renderConversationMarkdown):
|
|
3648
|
-
// session_text stores raw user text, so masking is render-time. `--no-redact`
|
|
3649
|
-
// opts out, same as the live path.
|
|
3650
1915
|
const redact = (text) => options.redact !== false ? redactSecrets(text) : text;
|
|
3651
1916
|
const content = redact((readSessionContent(session.id) ?? '').trim());
|
|
3652
1917
|
const digestRaw = readArchivedSessionPreview(session.id);
|
|
@@ -3654,12 +1919,6 @@ function renderArchivedSession(session, mode, options = {}) {
|
|
|
3654
1919
|
? { ...digestRaw, lastAssistant: redact(digestRaw.lastAssistant ?? '') }
|
|
3655
1920
|
: undefined;
|
|
3656
1921
|
if (mode === 'json') {
|
|
3657
|
-
// Minimal machine shape for a file-gone session: the durable user content
|
|
3658
|
-
// plus the cached preview digest, tagged archived. Deliberately NOT the full
|
|
3659
|
-
// { session, events } contract — there are no events to parse. Redact the
|
|
3660
|
-
// first-message-excerpt meta fields (topic/label/plan) too, matching the live
|
|
3661
|
-
// single-session --json (render.ts redactJson), so `--json` masks them unless
|
|
3662
|
-
// --no-redact is passed.
|
|
3663
1922
|
console.log(JSON.stringify({
|
|
3664
1923
|
session: {
|
|
3665
1924
|
...session,
|
|
@@ -3699,13 +1958,8 @@ function renderArchivedSession(session, mode, options = {}) {
|
|
|
3699
1958
|
async function renderSession(session, mode, filters, options = {}) {
|
|
3700
1959
|
const { hydrateSessionTranscript, findLocalSessionTranscripts } = await import('../lib/session/discover.js');
|
|
3701
1960
|
session = await hydrateSessionTranscript(session);
|
|
3702
|
-
// OpenCode stores sessions in SQLite; filePath is "db_path#session_id"
|
|
3703
1961
|
const realPath = session.filePath.split('#')[0];
|
|
3704
1962
|
if (!fs.existsSync(realPath)) {
|
|
3705
|
-
// The transcript file is gone, but the session's user turns are stored
|
|
3706
|
-
// durably in the local DB (session_text), so serve those instead of a bare
|
|
3707
|
-
// metadata dump (RUSH-2436). This is what makes an archived session still
|
|
3708
|
-
// render its prompts after `agents remove` / rm / a box reimage.
|
|
3709
1963
|
const archivedContent = readSessionContent(session.id);
|
|
3710
1964
|
if (archivedContent && archivedContent.trim() !== '') {
|
|
3711
1965
|
renderArchivedSession(session, mode, options);
|
|
@@ -3735,9 +1989,6 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
3735
1989
|
const modelStr = stats.models.length > 0 ? chalk.yellow(` ${stats.models.join(', ')}`) : '';
|
|
3736
1990
|
const branchStr = session.gitBranch ? chalk.gray(` (${session.gitBranch})`) : '';
|
|
3737
1991
|
const absTime = formatAbsoluteTime(session.timestamp);
|
|
3738
|
-
// Auto-inferred title headline (user /rename > Claude ai-title >
|
|
3739
|
-
// daemon-generated title > first-prompt topic) — the fastest way to
|
|
3740
|
-
// recognize which task this session is.
|
|
3741
1992
|
const title = sessionHeadline(session);
|
|
3742
1993
|
if (title) {
|
|
3743
1994
|
const badges = signalBadges(metaSignals(session));
|
|
@@ -3766,13 +2017,6 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
3766
2017
|
process.stdout.write(renderMarkdown(renderConversationMarkdown(events, { redact: options.redact !== false })));
|
|
3767
2018
|
return;
|
|
3768
2019
|
}
|
|
3769
|
-
// json — normalized events plus the durable session signals from the state
|
|
3770
|
-
// engine (plan text, PR, worktree, ticket). Pre-1.20.51 emitted a bare event
|
|
3771
|
-
// array; consumers that JSON.parse this now read `output.events` for the
|
|
3772
|
-
// array. See issue #743 (plan surfaced) and CHANGELOG for the shape change.
|
|
3773
|
-
// `todos` (RUSH-1503) is computed from the UNFILTERED transcript so the
|
|
3774
|
-
// checklist reflects true session state regardless of any `--include` filter;
|
|
3775
|
-
// it lets the Factory panel read the CLI's checklist instead of re-parsing.
|
|
3776
2020
|
const todos = inferSessionState(parsedEvents, { cwd: session.cwd }).todos;
|
|
3777
2021
|
process.stdout.write(renderJson(events, todos ? { ...session, todos } : session, { redact: options.redact !== false }));
|
|
3778
2022
|
}
|
|
@@ -3781,8 +2025,6 @@ function renderTopicCell(label, topic, query, visibleWidth, paddedWidth) {
|
|
|
3781
2025
|
const tpc = (topic ?? '').trim();
|
|
3782
2026
|
const sep = ' · ';
|
|
3783
2027
|
const raw = lbl && tpc ? `${lbl}${sep}${tpc}` : (lbl || tpc);
|
|
3784
|
-
// Width-aware: measure/truncate/pad by display cells, not String.length, so
|
|
3785
|
-
// ANSI escapes and wide (CJK/emoji) glyphs don't drift the column.
|
|
3786
2028
|
const visible = truncateToWidth(raw, visibleWidth);
|
|
3787
2029
|
const padding = ' '.repeat(Math.max(0, paddedWidth - stringWidth(visible)));
|
|
3788
2030
|
const labelEnd = lbl ? Math.min(lbl.length, visible.length) : 0;
|
|
@@ -3817,17 +2059,11 @@ function renderTopicCell(label, topic, query, visibleWidth, paddedWidth) {
|
|
|
3817
2059
|
}
|
|
3818
2060
|
return out + padding;
|
|
3819
2061
|
}
|
|
3820
|
-
/** Fallback machine-column width when a pool-derived width isn't supplied.
|
|
3821
|
-
* `pickerColumnsFor` normally computes `machineWidth` sized to the actual
|
|
3822
|
-
* hostnames, floored/capped by these bounds so common ids like `yosemite-s0`
|
|
3823
|
-
* (11) fit whole while a pathological hostname can't devour the topic column. */
|
|
3824
2062
|
const PICKER_MACHINE_W = 11;
|
|
3825
2063
|
const PICKER_MACHINE_MIN = 8;
|
|
3826
2064
|
const PICKER_MACHINE_MAX = 18;
|
|
3827
2065
|
const PICKER_MODEL_MIN = 6;
|
|
3828
2066
|
const PICKER_MODEL_MAX = 13;
|
|
3829
|
-
/** Column width that shows every compacted hostname in `machines` whole (one
|
|
3830
|
-
* trailing space for separation), bounded by MIN/MAX. */
|
|
3831
2067
|
function machineColumnWidth(machines, label) {
|
|
3832
2068
|
const widest = machines.reduce((w, m) => Math.max(w, stringWidth(label(m))), 0);
|
|
3833
2069
|
return Math.min(PICKER_MACHINE_MAX, Math.max(PICKER_MACHINE_MIN, widest + 1));
|
|
@@ -3836,12 +2072,6 @@ function modelColumnWidth(sessions) {
|
|
|
3836
2072
|
const widest = sessions.reduce((width, session) => (Math.max(width, session.model ? stringWidth(modelLabel(session.model)) : 0)), 0);
|
|
3837
2073
|
return Math.min(PICKER_MODEL_MAX, Math.max(PICKER_MODEL_MIN, widest + 1));
|
|
3838
2074
|
}
|
|
3839
|
-
/**
|
|
3840
|
-
* Compact display form for machine ids: strip the longest shared dash-delimited
|
|
3841
|
-
* prefix so "yosemite-s0"/"yosemite-s1" read as "s0"/"s1" while unrelated ids
|
|
3842
|
-
* ("zion", "mac-mini") stay whole. Stripping a *common* prefix can't collide,
|
|
3843
|
-
* and at least one segment is always kept.
|
|
3844
|
-
*/
|
|
3845
2075
|
export function machineLabeler(machines) {
|
|
3846
2076
|
const uniq = [...new Set(machines.filter(Boolean))];
|
|
3847
2077
|
if (uniq.length < 2)
|
|
@@ -3858,11 +2088,6 @@ export function machineLabeler(machines) {
|
|
|
3858
2088
|
return p.length > shared ? p.slice(shared).join('-') : m;
|
|
3859
2089
|
};
|
|
3860
2090
|
}
|
|
3861
|
-
/**
|
|
3862
|
-
* Column flags for a picker, computed once over the whole pool so every row
|
|
3863
|
-
* aligns: the machine column only earns its width when the listing spans more
|
|
3864
|
-
* than one box, the ticket column only when some row carries a PR/ticket ref.
|
|
3865
|
-
*/
|
|
3866
2091
|
export function pickerColumnsFor(sessions) {
|
|
3867
2092
|
const machines = sessions.map((s) => s.machine).filter((m) => !!m);
|
|
3868
2093
|
const distinct = [...new Set(machines)];
|
|
@@ -3875,22 +2100,12 @@ export function pickerColumnsFor(sessions) {
|
|
|
3875
2100
|
modelWidth: modelColumnWidth(sessions),
|
|
3876
2101
|
showTicket: sessions.some((s) => ticketLabel(s) !== ''),
|
|
3877
2102
|
showBookmark: (() => {
|
|
3878
|
-
// One read of the store per pool, not one per row.
|
|
3879
2103
|
const bookmarks = listBookmarks();
|
|
3880
2104
|
return bookmarks.size > 0 && sessions.some((s) => bookmarks.has(s.id));
|
|
3881
2105
|
})(),
|
|
3882
2106
|
};
|
|
3883
2107
|
}
|
|
3884
|
-
/** Width of the host-program column (`tmux→ghostty` is the long realistic case). */
|
|
3885
2108
|
const PICKER_HOST_W = 14;
|
|
3886
|
-
/**
|
|
3887
|
-
* Which program a live session is running in, for the picker's host column: the
|
|
3888
|
-
* immediate host app (`codium`, `ghostty`, `tmux`, `iterm`, …) and — when a tmux
|
|
3889
|
-
* session is currently being watched through a different app — the app it is
|
|
3890
|
-
* viewed in, as `tmux→ghostty`. A tmux session with no attached client stays a
|
|
3891
|
-
* bare `tmux`, which is exactly "running detached". Empty when the session has no
|
|
3892
|
-
* resolvable host (cloud rows, an unreadable process env).
|
|
3893
|
-
*/
|
|
3894
2109
|
export function liveHostLabel(a) {
|
|
3895
2110
|
if (!a?.host)
|
|
3896
2111
|
return '';
|
|
@@ -3902,16 +2117,9 @@ export function formatPickerLabel(s, query, cols = {}, ssh, host = '', bookmarke
|
|
|
3902
2117
|
const agentColor = colorAgent(shown);
|
|
3903
2118
|
const age = sessionAgeParts(s.timestamp, s.lastActivity);
|
|
3904
2119
|
const project = s.project || '-';
|
|
3905
|
-
// SSH-launch origin (live rows only): mirrors the flat listing's `ssh←<device>`
|
|
3906
|
-
// badge. Rendered as its OWN red segment before the topic cell — folding it into
|
|
3907
|
-
// the topic string loses the colour, because renderTopicCell strips ANSI and
|
|
3908
|
-
// re-whitens every slice. Its width is reserved from the topic budget below
|
|
3909
|
-
// (exactly like `wt`), so the fixed-width columns stay aligned.
|
|
3910
2120
|
const sshPlain = ssh ? (ssh.device ? `ssh←${ssh.device} ` : 'ssh ') : '';
|
|
3911
2121
|
const sshSeg = sshPlain ? chalk.red(sshPlain) : '';
|
|
3912
2122
|
const sshW = sshPlain ? stringWidth(sshPlain) : 0;
|
|
3913
|
-
// Orchestrator badge — same own-segment treatment as `ssh` above, for the same
|
|
3914
|
-
// reason: renderTopicCell would strip its colour if it rode inside the topic.
|
|
3915
2123
|
const team = teamBadge(s);
|
|
3916
2124
|
const teamSeg = team.plain ? chalk.green(team.plain) : '';
|
|
3917
2125
|
const tag = originTag(s) || teamTag(s);
|
|
@@ -3927,42 +2135,23 @@ export function formatPickerLabel(s, query, cols = {}, ssh, host = '', bookmarke
|
|
|
3927
2135
|
const ticketCell = cols.showTicket
|
|
3928
2136
|
? chalk.blue(padRight(truncate(ticketLabel(s) || '-', TICKET_W), TICKET_W + 1))
|
|
3929
2137
|
: '';
|
|
3930
|
-
// Which terminal/editor the session runs in — a tab in Ghostty vs a VS Code
|
|
3931
|
-
// panel vs a detached tmux pane is the thing you need to know to go find it.
|
|
3932
2138
|
const hostCell = cols.showHost
|
|
3933
2139
|
? chalk.gray(padRight(truncate(host || '-', PICKER_HOST_W - 1), PICKER_HOST_W))
|
|
3934
2140
|
: '';
|
|
3935
|
-
// The picker prepends a gutter (cursor, plus a checkbox in multi-select mode);
|
|
3936
|
-
// reserve it, plus the conditional columns, so the topic shrinks to fit and
|
|
3937
|
-
// rows never wrap.
|
|
3938
2141
|
const gutter = cols.gutter ?? 2;
|
|
3939
2142
|
const machineColW = cols.showMachine ? machineW : 0;
|
|
3940
2143
|
const ticketW = cols.showTicket ? TICKET_W + 1 : 0;
|
|
3941
2144
|
const hostW = cols.showHost ? PICKER_HOST_W : 0;
|
|
3942
2145
|
const wtW = wt ? stringWidth(wt) + 1 : 0;
|
|
3943
|
-
// Within a pool that HAS bookmarked rows the marker holds its 2 cells whether
|
|
3944
|
-
// this row is bookmarked or not, so the columns after it never jog; a pool
|
|
3945
|
-
// with none drops the column entirely (`showBookmark`) and costs nothing.
|
|
3946
2146
|
const bookmarkW = cols.showBookmark ? 2 : 0;
|
|
3947
2147
|
const bookmarkCell = cols.showBookmark ? (bookmarked ? chalk.yellow('★ ') : ' ') : '';
|
|
3948
|
-
// The same status word the flat listing shows, so a session that is `orphan`
|
|
3949
|
-
// or `crashed` reads that way in the browser too — not only in its preview.
|
|
3950
|
-
// Constant width whenever the column is on. `liveStatusCell` already pads its
|
|
3951
|
-
// word to LIVE_STATUS_W but returns an EMPTY cell (width 0) for a row with no
|
|
3952
|
-
// live match — blanks fill in for those, so the topic column does not jog on
|
|
3953
|
-
// exactly the rows that are not running.
|
|
3954
2148
|
const status = cols.showStatus ? liveStatusCell(live) : { cell: '', width: 0 };
|
|
3955
2149
|
const statusW = cols.showStatus ? LIVE_STATUS_W : 0;
|
|
3956
2150
|
const statusCell = cols.showStatus ? (status.cell || ' '.repeat(LIVE_STATUS_W)) : '';
|
|
3957
|
-
// Sized against the last-activity label alone; the creation field is then an
|
|
3958
|
-
// additive decision made against whatever width is left (see the flat listing).
|
|
3959
2151
|
const baseTopicW = terminalWidth() - gutter - bookmarkW - statusW - (10 + 9 + 8 + 16) - machineColW - hostW - ticketW - wtW - sshW - team.width - stringWidth(age.last) - 1;
|
|
3960
2152
|
const when = timeCell(age, baseTopicW);
|
|
3961
2153
|
const topicW = Math.max(MIN_TOPIC_W, baseTopicW - when.extraW);
|
|
3962
2154
|
return (bookmarkCell +
|
|
3963
|
-
// Truncated, not just padded: an indexed shortId is always 8 chars, but a
|
|
3964
|
-
// live row with no session id is named by its pid or cloud task, which can
|
|
3965
|
-
// run past the column and shunt every later column out of alignment.
|
|
3966
2155
|
chalk.white(padRight(truncate(s.shortId, 9), 10)) +
|
|
3967
2156
|
agentColor(padRight(truncate(shown, 8), 9)) +
|
|
3968
2157
|
chalk.yellow(padRight(truncate(versionStr, 7), 8)) +
|
|
@@ -3977,24 +2166,16 @@ export function formatPickerLabel(s, query, cols = {}, ssh, host = '', bookmarke
|
|
|
3977
2166
|
(wt ? wt + ' ' : '') +
|
|
3978
2167
|
when.text);
|
|
3979
2168
|
}
|
|
3980
|
-
/** Hints rotated above the picker so the flags/features stay discoverable. */
|
|
3981
2169
|
const PICKER_TIPS = [
|
|
3982
2170
|
'Tip: narrow with -a/--agent (e.g. -a codex), or --project <name> for another folder.',
|
|
3983
2171
|
"Tip: --all searches every directory; -D/--device <machine> folds in another box's sessions.",
|
|
3984
2172
|
'Tip: just type to fuzzy-search prompts and responses; press space to preview a session.',
|
|
3985
2173
|
'Tip: --since 2d / --until <date> bound the time window; pass a session id to open it directly.',
|
|
3986
2174
|
];
|
|
3987
|
-
/**
|
|
3988
|
-
* Pick a hint to show above the picker. Deterministic (keys off the pool size)
|
|
3989
|
-
* so it stays fixed across the picker's re-renders within a single run.
|
|
3990
|
-
*/
|
|
3991
2175
|
export function formatPickerTip(sessions) {
|
|
3992
2176
|
return chalk.gray(PICKER_TIPS[sessions.length % PICKER_TIPS.length]);
|
|
3993
2177
|
}
|
|
3994
2178
|
export async function pickSessionInteractive(sessions, message = 'Search sessions:', initialSearch, hiddenCount = 0, enterHint, scope) {
|
|
3995
|
-
// The hidden-session footer is console.log'd above the Inquirer prompt, so it
|
|
3996
|
-
// scrolls the viewport the picker can't measure; tell the picker to reserve for
|
|
3997
|
-
// it (see pickerPageSize) so the preview and the footer stay on screen together.
|
|
3998
2179
|
let linesAbovePrompt = 0;
|
|
3999
2180
|
if (hiddenCount > 0) {
|
|
4000
2181
|
console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
|
|
@@ -4007,8 +2188,6 @@ export async function pickSessionInteractive(sessions, message = 'Search session
|
|
|
4007
2188
|
subtitle: formatPickerTip(sessions),
|
|
4008
2189
|
sessions,
|
|
4009
2190
|
filter: (query) => {
|
|
4010
|
-
// No query: show the full pool (picker viewport still paginates via pageSize).
|
|
4011
|
-
// Typing: search the full pool.
|
|
4012
2191
|
if (!query.trim())
|
|
4013
2192
|
return sessions;
|
|
4014
2193
|
return filterSessionsByQuery(sessions, query, scope);
|
|
@@ -4026,36 +2205,21 @@ export async function pickSessionInteractive(sessions, message = 'Search session
|
|
|
4026
2205
|
throw err;
|
|
4027
2206
|
}
|
|
4028
2207
|
}
|
|
4029
|
-
/** True when the peer wasn't a dialable device; prints one clear line so a
|
|
4030
|
-
* remote pick never dead-ends silently. */
|
|
4031
2208
|
function warnNoPeerTarget(machine, session) {
|
|
4032
2209
|
console.log(chalk.yellow(`Session ${session.shortId} lives on ${machine}, which isn't a reachable device right now.`));
|
|
4033
2210
|
console.log(chalk.gray(`Register/wake it (ag devices), or run there: agents ssh ${machine}`));
|
|
4034
2211
|
}
|
|
4035
|
-
/**
|
|
4036
|
-
* Row-id prefix for a live session whose agent has not reported a session id yet
|
|
4037
|
-
* (a booting harness, a queued teammate). The browser lists these so nothing live
|
|
4038
|
-
* is hidden, but they address no transcript — {@link isIdlessLiveRow} is what the
|
|
4039
|
-
* pick handler checks before trying to read or resume one.
|
|
4040
|
-
*/
|
|
4041
2212
|
export const LIVE_ROW_PREFIX = 'live:';
|
|
4042
2213
|
function isIdlessLiveRow(s) {
|
|
4043
2214
|
return s.id.startsWith(LIVE_ROW_PREFIX);
|
|
4044
2215
|
}
|
|
4045
2216
|
export async function handlePickedSession(picked) {
|
|
4046
|
-
// A live row with no session id has no transcript to open and no id to resume
|
|
4047
|
-
// by; say where the process is instead of dead-ending on an unreadable path.
|
|
4048
2217
|
if (isIdlessLiveRow(picked.session)) {
|
|
4049
2218
|
const where = picked.session.machine ? ` on ${picked.session.machine}` : '';
|
|
4050
2219
|
console.log(chalk.yellow(`This session hasn't reported a session id yet — nothing to open${where}.`));
|
|
4051
2220
|
console.log(chalk.gray(`Watch for it with: agents sessions --active${picked.session.machine ? ` --device ${picked.session.machine}` : ''}`));
|
|
4052
2221
|
return;
|
|
4053
2222
|
}
|
|
4054
|
-
// Reading and resuming are on DIFFERENT machines' terms, and conflating them
|
|
4055
|
-
// is what RUSH-2022 was. Reading follows the FILE: a synced mirror sits on
|
|
4056
|
-
// this disk, so a peer-owned row with no readable local transcript has to be
|
|
4057
|
-
// read on the peer. Resuming follows the HARNESS STATE, which is on the
|
|
4058
|
-
// owning machine whatever the transcript's location — a mirror included.
|
|
4059
2223
|
if (picked.action === 'view') {
|
|
4060
2224
|
const readFrom = transcriptOnPeerOf(picked.session);
|
|
4061
2225
|
if (readFrom) {
|
|
@@ -4071,22 +2235,11 @@ export async function handlePickedSession(picked) {
|
|
|
4071
2235
|
return;
|
|
4072
2236
|
await resumeSessionInPlace(picked.session);
|
|
4073
2237
|
}
|
|
4074
|
-
/**
|
|
4075
|
-
* Resume `session` on the machine that owns it, when that is not this one.
|
|
4076
|
-
* Returns true when it handled the resume (hopped, or reported an unreachable
|
|
4077
|
-
* owner), false when the session is local and the caller should take over here.
|
|
4078
|
-
*
|
|
4079
|
-
* The one hop for a foreground, one-session-at-a-time resume — shared by the
|
|
4080
|
-
* `agents sessions` picker and by `sessions resume`'s no-tab-backend path, which
|
|
4081
|
-
* would otherwise reach `resumeSessionInPlace` and be refused (RUSH-2022).
|
|
4082
|
-
*/
|
|
4083
2238
|
async function resumeOnOwnerIfRemote(session) {
|
|
4084
2239
|
const owner = sessionOwnerDevice(session);
|
|
4085
2240
|
if (!owner)
|
|
4086
2241
|
return false;
|
|
4087
2242
|
console.log(chalk.gray(`Resuming ${session.shortId} on ${owner} over SSH...`));
|
|
4088
|
-
// `agents sessions resume <id>` is the strict single-session path — one pick, one
|
|
4089
|
-
// resume.
|
|
4090
2243
|
const rc = await runOnPeer(['sessions', 'resume', session.id], owner, {
|
|
4091
2244
|
tty: true,
|
|
4092
2245
|
env: { [RESUME_PINNED_ENV]: '1' },
|
|
@@ -4096,21 +2249,7 @@ async function resumeOnOwnerIfRemote(session) {
|
|
|
4096
2249
|
warnNoPeerTarget(owner, session);
|
|
4097
2250
|
return true;
|
|
4098
2251
|
}
|
|
4099
|
-
/**
|
|
4100
|
-
* Resume a session in the current terminal — a foreground takeover of this
|
|
4101
|
-
* process. Used by the single-select picker and by `sessions resume` when the
|
|
4102
|
-
* chosen destination is "in place" (unknown emulator / off-macOS, single pick).
|
|
4103
|
-
* Falls back to the same resume invocation against the current version when the
|
|
4104
|
-
* version-pinned launcher is genuinely missing.
|
|
4105
|
-
*/
|
|
4106
2252
|
export async function resumeSessionInPlace(session) {
|
|
4107
|
-
// This function is the LOCAL takeover, and every caller is responsible for
|
|
4108
|
-
// routing a peer-owned session before it gets here (the picker above,
|
|
4109
|
-
// `agents sessions resume`). Reaching it with one anyway means a
|
|
4110
|
-
// caller skipped that step, so refuse rather than start the harness against
|
|
4111
|
-
// state this box does not have — the `fs.existsSync(session.cwd)` fallback
|
|
4112
|
-
// just below is exactly how that used to pass unnoticed, quietly swapping in
|
|
4113
|
-
// `process.cwd()` when the recorded directory was a peer's path (RUSH-2022).
|
|
4114
2253
|
const owner = sessionOwnerDevice(session);
|
|
4115
2254
|
if (owner) {
|
|
4116
2255
|
console.error(chalk.red(`Session ${session.shortId} belongs to ${owner} — it cannot resume on this machine.`));
|
|
@@ -4125,12 +2264,6 @@ export async function resumeSessionInPlace(session) {
|
|
|
4125
2264
|
console.log(chalk.gray(`Resuming: ${resume.join(' ')} (cwd: ${cwd})`));
|
|
4126
2265
|
await spawnResumeCommand(resume, cwd);
|
|
4127
2266
|
}
|
|
4128
|
-
/**
|
|
4129
|
-
* Relaunch this CLI's one recovery path. The owning device resolves account
|
|
4130
|
-
* health and either performs exact-home native resume or same-harness
|
|
4131
|
-
* `/continue`; callers never guess which version home can see the transcript.
|
|
4132
|
-
* `portable` is for an SSH/terminal-engine command that executes on a peer.
|
|
4133
|
-
*/
|
|
4134
2267
|
export function buildSessionRecoveryCommand(session, portable = false) {
|
|
4135
2268
|
const args = sessionRecoveryRunArgs(session);
|
|
4136
2269
|
if (portable)
|
|
@@ -4138,17 +2271,6 @@ export function buildSessionRecoveryCommand(session, portable = false) {
|
|
|
4138
2271
|
const invocation = getAgentsInvocation(args);
|
|
4139
2272
|
return [invocation.command, ...invocation.args];
|
|
4140
2273
|
}
|
|
4141
|
-
/**
|
|
4142
|
-
* Map a resume argv to the spawn(command, args, {shell}) triple.
|
|
4143
|
-
*
|
|
4144
|
-
* On Windows the agent launcher is a `.cmd`/PATHEXT shim and needs shell:true.
|
|
4145
|
-
* With shell:true, Node concatenates args into the cmd.exe line unescaped
|
|
4146
|
-
* (DEP0190 + injection). A session.id derived from a filename can carry
|
|
4147
|
-
* metacharacters (`&|<>`); compose a fully-quoted line and pass an EMPTY args
|
|
4148
|
-
* array so cmd.exe cannot reparse them (RUSH-1753). See composeWin32CommandLine.
|
|
4149
|
-
*
|
|
4150
|
-
* `platform` is injectable so the win32 shell path is unit-testable on any host.
|
|
4151
|
-
*/
|
|
4152
2274
|
export function resumeSpawnInvocation(cmd, platform = process.platform) {
|
|
4153
2275
|
const shell = needsWindowsShell(cmd[0], platform);
|
|
4154
2276
|
if (shell) {
|
|
@@ -4160,17 +2282,6 @@ export function resumeSpawnInvocation(cmd, platform = process.platform) {
|
|
|
4160
2282
|
}
|
|
4161
2283
|
return { command: cmd[0], args: cmd.slice(1), shell: false };
|
|
4162
2284
|
}
|
|
4163
|
-
/**
|
|
4164
|
-
* Spawn a resume command as a foreground takeover (inherited stdio), resolving
|
|
4165
|
-
* when it exits. On Windows the agent launcher is a `.cmd`/PATHEXT shim that
|
|
4166
|
-
* `spawn` can't exec directly — a bare-name `shell:false` spawn throws
|
|
4167
|
-
* `EFTYPE`/`ENOENT` there — so we go through the shell via `needsWindowsShell`.
|
|
4168
|
-
* When shell:true, argv is composed via resumeSpawnInvocation (quoted line +
|
|
4169
|
-
* empty args) so an untrusted session.id cannot inject through cmd.exe.
|
|
4170
|
-
* The spawn is guarded because such a failure can be thrown synchronously;
|
|
4171
|
-
* without the guard it would surface under an unrelated "Failed to discover
|
|
4172
|
-
* sessions" catch upstream instead of a truthful launch error.
|
|
4173
|
-
*/
|
|
4174
2285
|
function spawnResumeCommand(cmd, cwd) {
|
|
4175
2286
|
return new Promise((resolve) => {
|
|
4176
2287
|
let child;
|
|
@@ -4197,40 +2308,15 @@ function spawnResumeCommand(cmd, cwd) {
|
|
|
4197
2308
|
child.on('close', () => resolve());
|
|
4198
2309
|
});
|
|
4199
2310
|
}
|
|
4200
|
-
/**
|
|
4201
|
-
* Build the shell command that resumes a picked session.
|
|
4202
|
-
*
|
|
4203
|
-
* When the session's originating version is known, uses the version-pinned
|
|
4204
|
-
* binary (e.g. `claude@2.1.138`) so the resume always runs in the same
|
|
4205
|
-
* isolated HOME where the JSONL was written — regardless of which version is
|
|
4206
|
-
* currently the default. Falls back to the bare shim when version is unknown.
|
|
4207
|
-
*
|
|
4208
|
-
*/
|
|
4209
|
-
/**
|
|
4210
|
-
* The agent's own resume invocation, given whichever launcher we resolved.
|
|
4211
|
-
* Keeping the verb in one place is what lets the version-pinned and fallback
|
|
4212
|
-
* paths stay in agreement — they previously drifted into `/continue`, which is
|
|
4213
|
-
* not a command either CLI has.
|
|
4214
|
-
*/
|
|
4215
2311
|
function resumeArgv(agent, id, launcher) {
|
|
4216
2312
|
switch (agent) {
|
|
4217
2313
|
case 'claude': return [launcher, '--resume', id];
|
|
4218
2314
|
case 'codex': return [launcher, 'resume', id];
|
|
4219
2315
|
case 'opencode': return [launcher, '--session', id];
|
|
4220
|
-
// Muse interactive resume is a subcommand: `muse resume <uuid>`.
|
|
4221
2316
|
case 'muse': return [launcher, 'resume', id];
|
|
4222
2317
|
default: return null;
|
|
4223
2318
|
}
|
|
4224
2319
|
}
|
|
4225
|
-
/**
|
|
4226
|
-
* Absolute path of the on-disk versioned alias, or null when it isn't there.
|
|
4227
|
-
*
|
|
4228
|
-
* Resume must not resolve `<cli>@<version>` by bare name. The shims directory is
|
|
4229
|
-
* deliberately absent from PATH for an isolated install — that is precisely what
|
|
4230
|
-
* `--isolated` promises — so a PATH lookup can never find the alias, and resume
|
|
4231
|
-
* degraded to the fallback 100% of the time for isolated copies. Mirrors the
|
|
4232
|
-
* resolution `buildExecCommand` already does for `agents run`.
|
|
4233
|
-
*/
|
|
4234
2320
|
function versionedAliasIfPresent(agent, version) {
|
|
4235
2321
|
const cli = AGENTS[agent]?.cliCommand ?? agent;
|
|
4236
2322
|
const base = path.join(getShimsDir(), `${cli}@${version}`);
|
|
@@ -4244,8 +2330,6 @@ export function buildResumeCommand(session) {
|
|
|
4244
2330
|
if (!sessionAgentSupportsResume(session.agent))
|
|
4245
2331
|
return null;
|
|
4246
2332
|
switch (session.agent) {
|
|
4247
|
-
// opencode sessions are shared across versions, so resume is deliberately NOT
|
|
4248
|
-
// version-pinned — it always goes through the plain launcher.
|
|
4249
2333
|
case 'opencode':
|
|
4250
2334
|
return resumeArgv('opencode', session.id, 'opencode');
|
|
4251
2335
|
case 'claude':
|
|
@@ -4254,8 +2338,6 @@ export function buildResumeCommand(session) {
|
|
|
4254
2338
|
const cli = AGENTS[session.agent]?.cliCommand ?? session.agent;
|
|
4255
2339
|
if (session.version) {
|
|
4256
2340
|
const alias = versionedAliasIfPresent(session.agent, session.version);
|
|
4257
|
-
// Absolute path when the alias exists; otherwise the bare versioned name,
|
|
4258
|
-
// which still resolves for a non-isolated install whose shims are on PATH.
|
|
4259
2341
|
return resumeArgv(session.agent, session.id, alias ?? `${cli}@${session.version}`);
|
|
4260
2342
|
}
|
|
4261
2343
|
return resumeArgv(session.agent, session.id, cli);
|
|
@@ -4264,15 +2346,6 @@ export function buildResumeCommand(session) {
|
|
|
4264
2346
|
return null;
|
|
4265
2347
|
}
|
|
4266
2348
|
}
|
|
4267
|
-
// ---------------------------------------------------------------------------
|
|
4268
|
-
// Cloud session source (--cloud)
|
|
4269
|
-
// ---------------------------------------------------------------------------
|
|
4270
|
-
/**
|
|
4271
|
-
* Handle `agents sessions --cloud [id] [filters]`.
|
|
4272
|
-
* - Without id: list captured cloud-runs, optionally as JSON.
|
|
4273
|
-
* - With id: fetch the jsonl, parse with the recorded format, render via
|
|
4274
|
-
* the same pipeline as local sessions (summary / markdown / json).
|
|
4275
|
-
*/
|
|
4276
2349
|
async function runCloudSessions(query, options) {
|
|
4277
2350
|
const { discoverCloudSessions, ensureCloudSessionCached } = await import('../lib/session/cloud.js');
|
|
4278
2351
|
let filterOpts;
|
|
@@ -4331,11 +2404,8 @@ async function runCloudSessions(query, options) {
|
|
|
4331
2404
|
process.exit(1);
|
|
4332
2405
|
}
|
|
4333
2406
|
cachedSpinner?.stop();
|
|
4334
|
-
// Ensure the SessionMeta points at the local cache path for renderSession.
|
|
4335
2407
|
await renderSession({ ...meta, filePath: cachedPath }, mode, filterOpts, options);
|
|
4336
2408
|
}
|
|
4337
|
-
/** Resolve the harness portion of a sessions selector with the CLI's canonical
|
|
4338
|
-
* alias and single-typo rules. Kept pure so focus and the browser cannot drift. */
|
|
4339
2409
|
export function resolveSessionAgentName(name) {
|
|
4340
2410
|
const normalized = name.toLowerCase();
|
|
4341
2411
|
if (SESSION_AGENTS.includes(normalized)) {
|
|
@@ -4368,41 +2438,18 @@ function formatSearchMessage(options) {
|
|
|
4368
2438
|
return 'Search sessions:';
|
|
4369
2439
|
return `Search sessions (${filters.join(', ')}):`;
|
|
4370
2440
|
}
|
|
4371
|
-
/**
|
|
4372
|
-
* The single entry point for turning a `sessions <query>` argument into rows.
|
|
4373
|
-
*
|
|
4374
|
-
* An id-shaped query — a complete id OR a hex short-id/prefix (looksLikeSessionId)
|
|
4375
|
-
* — resolves by id alone, through the index, and never falls back to content
|
|
4376
|
-
* search (a bare id must not surface every transcript that merely mentions it).
|
|
4377
|
-
* A genuine search phrase keeps the ranked metadata+content search.
|
|
4378
|
-
*/
|
|
4379
2441
|
export function resolveSessionQuery(pool, query, options = {}) {
|
|
4380
|
-
// Normalize ONCE here. isCompleteSessionId trims but resolveSessionById does
|
|
4381
|
-
// not, so a padded id ("<uuid> ", e.g. pasted from a terminal) would classify
|
|
4382
|
-
// as complete and then miss the id lookup — reporting a session that IS on
|
|
4383
|
-
// this machine as absent.
|
|
4384
2442
|
const normalized = query.trim();
|
|
4385
2443
|
const completeId = isCompleteSessionId(normalized);
|
|
4386
2444
|
const byIdMatches = resolveSessionById(pool, normalized);
|
|
4387
2445
|
if (byIdMatches.length > 0)
|
|
4388
2446
|
return { matches: byIdMatches, byId: true, completeId };
|
|
4389
2447
|
if (looksLikeSessionId(normalized)) {
|
|
4390
|
-
// Any id-shaped query — a complete id OR a bare hex short-id/prefix —
|
|
4391
|
-
// resolves by id ONLY, never by content. The pool is a minority of the
|
|
4392
|
-
// index (measured: 2,798 of 7,614 rows) because it re-reads live agent homes
|
|
4393
|
-
// and skips whole classes of indexed session, so a pool miss isn't absence:
|
|
4394
|
-
// ask the index directly, the same authoritative lookup `fork` and `exec`
|
|
4395
|
-
// use. And an id that resolves to nothing must report "no session with that
|
|
4396
|
-
// id" — NOT fall back to fuzzy content search. A short id like "d3470b57"
|
|
4397
|
-
// otherwise surfaces every transcript that merely MENTIONS the string (a
|
|
4398
|
-
// resume prompt echoes the parent id into the body of many later sessions).
|
|
4399
2448
|
const matches = options.indexFallback === false ? [] : findSessionsById(normalized);
|
|
4400
2449
|
return { matches, byId: true, completeId };
|
|
4401
2450
|
}
|
|
4402
2451
|
return { matches: filterSessionsByQuery(pool, normalized, options.scope), byId: false, completeId };
|
|
4403
2452
|
}
|
|
4404
|
-
/** Explain an ambiguous resolution. Only a short id can be lengthened: a complete
|
|
4405
|
-
* id is already maximal, and a search phrase was never an id to begin with. */
|
|
4406
2453
|
function ambiguityHint(byId, completeId) {
|
|
4407
2454
|
if (completeId)
|
|
4408
2455
|
return 'That is already a complete id — these rows share it as a prefix.';
|
|
@@ -4410,18 +2457,9 @@ function ambiguityHint(byId, completeId) {
|
|
|
4410
2457
|
? 'Pass a longer ID to narrow it down.'
|
|
4411
2458
|
: 'That matched on text, not an id. Pass a session id, or narrow the search.';
|
|
4412
2459
|
}
|
|
4413
|
-
/** Explain a complete-id miss, which no local rephrasing can fix. Echoes the
|
|
4414
|
-
* normalized id so a pasted, padded argument doesn't produce an unrunnable hint.
|
|
4415
|
-
* Used only where NO fleet sweep ran (a `--local` lookup, an `--artifacts`
|
|
4416
|
-
* listing, or a peer answering a parent's sweep) — the fleet-swept miss uses
|
|
4417
|
-
* {@link fleetNotFoundMessage}. Never tells the user to pass `--device`: fleet
|
|
4418
|
-
* search is already the default, so a device flag can't find what the id missed. */
|
|
4419
2460
|
function notFoundByIdMessage(query) {
|
|
4420
2461
|
return [chalk.red(`No session with id ${query.trim()} on this machine.`)];
|
|
4421
2462
|
}
|
|
4422
|
-
/** Honest result of a fleet sweep that found nothing: it says what the sweep
|
|
4423
|
-
* actually did — how many peers answered, which were unreachable — instead of
|
|
4424
|
-
* dead-ending on "search with --device <host>" after the search already ran. */
|
|
4425
2463
|
export function fleetNotFoundMessage(query, deviceCount, unreachable) {
|
|
4426
2464
|
const id = query.trim();
|
|
4427
2465
|
if (deviceCount === 0) {
|
|
@@ -4437,7 +2475,6 @@ export function fleetNotFoundMessage(query, deviceCount, unreachable) {
|
|
|
4437
2475
|
}
|
|
4438
2476
|
return lines;
|
|
4439
2477
|
}
|
|
4440
|
-
/** Filter and rank sessions by a multi-term search query across metadata and content. */
|
|
4441
2478
|
export function filterSessionsByQuery(sessions, query, scope) {
|
|
4442
2479
|
const trimmed = query?.trim().toLowerCase() || '';
|
|
4443
2480
|
if (!trimmed)
|
|
@@ -4448,20 +2485,12 @@ export function filterSessionsByQuery(sessions, query, scope) {
|
|
|
4448
2485
|
return sessions.filter((session) => session.agent === agent && session.version === version);
|
|
4449
2486
|
}
|
|
4450
2487
|
const terms = trimmed.split(/\s+/).filter(Boolean);
|
|
4451
|
-
// Hydrate FTS hits missing from the pool (PHNX-2767), then drop hits that
|
|
4452
|
-
// fail --project/--agent/--routine so those flags are not undone by the union.
|
|
4453
2488
|
const contentIndex = scopedContentIndex(sessions, trimmed, scope);
|
|
4454
|
-
// If the query exactly matches a session label, short-circuit the structural
|
|
4455
|
-
// scorer (which would otherwise surface every session whose topic happens to
|
|
4456
|
-
// contain the same words) and return only the label hits.
|
|
4457
2489
|
const EXACT_LABEL_SCORE = 1_000_000;
|
|
4458
2490
|
const exactLabelHits = [...contentIndex.values()].filter(s => (s._bm25Score ?? 0) >= EXACT_LABEL_SCORE);
|
|
4459
2491
|
if (exactLabelHits.length > 0) {
|
|
4460
2492
|
return exactLabelHits.sort((a, b) => (b._bm25Score ?? 0) - (a._bm25Score ?? 0));
|
|
4461
2493
|
}
|
|
4462
|
-
// The listing pool is a page of the index. FTS hits the whole index. Union
|
|
4463
|
-
// so a grep-visible transcript is returned even when it missed the page
|
|
4464
|
-
// (PHNX-2767).
|
|
4465
2494
|
const poolById = new Map(sessions.map(s => [s.id, s]));
|
|
4466
2495
|
for (const [id, hit] of contentIndex) {
|
|
4467
2496
|
if (!poolById.has(id))
|
|
@@ -4470,7 +2499,6 @@ export function filterSessionsByQuery(sessions, query, scope) {
|
|
|
4470
2499
|
return [...poolById.values()]
|
|
4471
2500
|
.map(session => ({ session, score: scoreSessionQuery(session, terms) }))
|
|
4472
2501
|
.filter(entry => {
|
|
4473
|
-
// Include if scored by topic/project/etc, or matched by content search
|
|
4474
2502
|
if (entry.score > 0)
|
|
4475
2503
|
return true;
|
|
4476
2504
|
const contentMatch = contentIndex.get(entry.session.id);
|
|
@@ -4491,7 +2519,6 @@ export function filterSessionsByQuery(sessions, query, scope) {
|
|
|
4491
2519
|
return new Date(b.session.timestamp).getTime() - new Date(a.session.timestamp).getTime();
|
|
4492
2520
|
})
|
|
4493
2521
|
.map(entry => {
|
|
4494
|
-
// Attach content match terms for highlighting
|
|
4495
2522
|
const cm = contentIndex.get(entry.session.id);
|
|
4496
2523
|
if (cm && cm._matchedTerms) {
|
|
4497
2524
|
return { ...cm };
|
|
@@ -4539,17 +2566,6 @@ function scoreSessionQuery(session, terms) {
|
|
|
4539
2566
|
}
|
|
4540
2567
|
return score;
|
|
4541
2568
|
}
|
|
4542
|
-
/**
|
|
4543
|
-
* Narrow a session list by --project and --agent before search resolution.
|
|
4544
|
-
* Without this, a query like "scoped search" could match sessions in BOTH
|
|
4545
|
-
* the project you specified AND elsewhere, producing an ambiguity error
|
|
4546
|
-
* even though the user already pointed at the correct scope.
|
|
4547
|
-
*
|
|
4548
|
-
* PHNX-2767 hydrates FTS hits missing from the listing pool *after* this
|
|
4549
|
-
* runs, so callers that search must also pass the same scope into
|
|
4550
|
-
* `filterSessionsByQuery` / `scopedContentIndex` or the union
|
|
4551
|
-
* reintroduces the rows this function just dropped.
|
|
4552
|
-
*/
|
|
4553
2569
|
export function applyScopeFilters(sessions, scope) {
|
|
4554
2570
|
let filtered = sessions;
|
|
4555
2571
|
if (scope.project) {
|
|
@@ -4561,7 +2577,6 @@ export function applyScopeFilters(sessions, scope) {
|
|
|
4561
2577
|
});
|
|
4562
2578
|
}
|
|
4563
2579
|
if (scope.agent) {
|
|
4564
|
-
// Accept "claude" or "claude@2.1.112" / "claude@default" / "claude@latest". Version suffix narrows further.
|
|
4565
2580
|
const [wantAgent, rawVersion] = scope.agent.split('@');
|
|
4566
2581
|
const resolvedAgent = resolveAgentName(wantAgent);
|
|
4567
2582
|
const wantVersion = resolvedAgent ? resolveVersionAliasLoose(resolvedAgent, rawVersion) : rawVersion;
|
|
@@ -4585,12 +2600,6 @@ export function applyScopeFilters(sessions, scope) {
|
|
|
4585
2600
|
}
|
|
4586
2601
|
return filtered;
|
|
4587
2602
|
}
|
|
4588
|
-
/**
|
|
4589
|
-
* PHNX-2767 hydrates FTS hits missing from the listing pool. That union is
|
|
4590
|
-
* unscoped on purpose (cwd/limit are a page of the index, not a filter).
|
|
4591
|
-
* `--project` / `--agent` / `--routine` ARE filters: drop hydrated hits that
|
|
4592
|
-
* fail them so content search cannot undo a scope the user already set.
|
|
4593
|
-
*/
|
|
4594
2603
|
function scopedContentIndex(sessions, query, scope) {
|
|
4595
2604
|
const hits = searchContentIndex(sessions, query);
|
|
4596
2605
|
if (!scope || (!scope.agent && !scope.project && !scope.routine))
|
|
@@ -4651,19 +2660,12 @@ async function renderOneSession(query, mode, scope) {
|
|
|
4651
2660
|
if (looksLikeSessionId(query)) {
|
|
4652
2661
|
const outcome = await resolveSessionMetadataValue(query, scope);
|
|
4653
2662
|
if (outcome.kind === 'partial') {
|
|
4654
|
-
// RUSH-2492: an unreachable peer is a warning, not a hard failure. The
|
|
4655
|
-
// resolver already resolves an id found on the reachable fleet (SES-9a),
|
|
4656
|
-
// so reaching here means the session was not found on any device we
|
|
4657
|
-
// COULD reach — it may live on an unreachable peer we could not check.
|
|
4658
2663
|
const offline = outcome.failedPeers;
|
|
4659
2664
|
console.error(chalk.yellow(`Warning: ${offline.length} device(s) unreachable, not checked: ${offline.join(', ')}`));
|
|
4660
2665
|
console.error(chalk.red(`No session matching "${query}" on any reachable device (${offline.length} unreachable, not checked).`));
|
|
4661
2666
|
console.error(chalk.gray(' If it lives on an offline box, wake it (agents devices) or run there: agents ssh <device>'));
|
|
4662
2667
|
process.exit(1);
|
|
4663
2668
|
}
|
|
4664
|
-
// An index miss can be a transcript created since the last incremental
|
|
4665
|
-
// scan, or a Claude history alias. Fall through to the established
|
|
4666
|
-
// discovery/history resolver below only on that cold miss.
|
|
4667
2669
|
if (outcome.kind === 'ambiguous') {
|
|
4668
2670
|
console.error(chalk.red(`Multiple sessions match "${query}" across the fleet:`));
|
|
4669
2671
|
for (const candidate of outcome.candidates) {
|
|
@@ -4719,13 +2721,6 @@ async function renderOneSession(query, mode, scope) {
|
|
|
4719
2721
|
let queryMatches = resolution.matches;
|
|
4720
2722
|
let byId = resolution.byId;
|
|
4721
2723
|
const completeId = resolution.completeId;
|
|
4722
|
-
// Widen to the transcript content index only for a genuine search phrase.
|
|
4723
|
-
// ANY id-shaped query (a complete id OR a hex short-id/prefix) names a
|
|
4724
|
-
// specific session; widening could only surface a DIFFERENT session that
|
|
4725
|
-
// happens to MENTION the id — which is what made `sessions <uuid>` render an
|
|
4726
|
-
// unrelated transcript and `sessions <shortid>` list every session that
|
|
4727
|
-
// echoes the id in a resume prompt. Gate on looksLikeSessionId, not just
|
|
4728
|
-
// completeId, so a short id resolves to "no match" rather than fuzzy content.
|
|
4729
2724
|
if (queryMatches.length === 0 && !looksLikeSessionId(query)) {
|
|
4730
2725
|
const contentResults = scopedContentIndex(allSessions, query, scope);
|
|
4731
2726
|
if (contentResults.size > 0) {
|
|
@@ -4754,22 +2749,12 @@ async function renderOneSession(query, mode, scope) {
|
|
|
4754
2749
|
}
|
|
4755
2750
|
}
|
|
4756
2751
|
else if (byId) {
|
|
4757
|
-
// Not on this machine. A UUID names ONE session, so before giving up ask
|
|
4758
|
-
// the fleet — the session may live only on a peer (the whole point of
|
|
4759
|
-
// RUSH-2024). Skip the sweep when the caller pinned --local, or when we
|
|
4760
|
-
// are ourselves a peer answering a parent's sweep (AGENTS_SESSIONS_LOCAL),
|
|
4761
|
-
// so a locate never recurses. On a single remote hit we hand rendering to
|
|
4762
|
-
// that peer; a multi-host hit surfaces the conflict; a miss keeps the
|
|
4763
|
-
// local not-found message. No FTS fallback either way.
|
|
4764
2752
|
if (shouldFanOutForId(query, scope.local)) {
|
|
4765
2753
|
const outcome = await resolveSessionAcrossFleet(query, mode, scope.hosts);
|
|
4766
2754
|
if (outcome.kind === 'rendered')
|
|
4767
2755
|
return;
|
|
4768
2756
|
if (outcome.kind === 'conflict')
|
|
4769
2757
|
process.exit(1);
|
|
4770
|
-
// The fleet sweep already ran and found nothing — report what actually
|
|
4771
|
-
// happened, never "search with --device <host>" (fleet search is the
|
|
4772
|
-
// default, so a device flag would only re-run the same miss).
|
|
4773
2758
|
fleetNotFoundMessage(query, outcome.deviceCount, outcome.unreachable).forEach(l => console.error(l));
|
|
4774
2759
|
process.exit(1);
|
|
4775
2760
|
}
|
|
@@ -4811,19 +2796,6 @@ async function renderOneSession(query, mode, scope) {
|
|
|
4811
2796
|
process.exit(1);
|
|
4812
2797
|
}
|
|
4813
2798
|
}
|
|
4814
|
-
/**
|
|
4815
|
-
* Whether a missed local id lookup should widen to a cross-machine sweep.
|
|
4816
|
-
*
|
|
4817
|
-
* Gate (all must hold):
|
|
4818
|
-
* - the query is id-shaped (`looksLikeSessionId`) — only an identifier resolves
|
|
4819
|
-
* across the fleet; a search phrase never does.
|
|
4820
|
-
* - not `--local` — the caller opted out of cross-machine lookup (deterministic
|
|
4821
|
-
* local behavior for scripts, RUSH-2024 acceptance: "--local still restricts").
|
|
4822
|
-
* - `AGENTS_SESSIONS_LOCAL` is unset — we are not ourselves a peer answering a
|
|
4823
|
-
* parent's sweep, so a locate can never recurse (RUSH-2024: avoid double-fan-out).
|
|
4824
|
-
*
|
|
4825
|
-
* Pure + exported so the gate is unit-tested without driving discovery / SSH.
|
|
4826
|
-
*/
|
|
4827
2799
|
function shouldFanOutForId(query, local) {
|
|
4828
2800
|
if (local === true)
|
|
4829
2801
|
return false;
|
|
@@ -4831,45 +2803,15 @@ function shouldFanOutForId(query, local) {
|
|
|
4831
2803
|
return false;
|
|
4832
2804
|
return looksLikeSessionId(query);
|
|
4833
2805
|
}
|
|
4834
|
-
/** The render-mode flag a peer's `agents sessions <id>` must carry so the remote
|
|
4835
|
-
* summary matches the mode the user asked for. `summary` is the peer's default,
|
|
4836
|
-
* so it needs no flag; the others map 1:1 to a CLI flag. */
|
|
4837
2806
|
function modeFlag(mode) {
|
|
4838
2807
|
if (mode === 'markdown')
|
|
4839
2808
|
return '--markdown';
|
|
4840
2809
|
if (mode === 'json')
|
|
4841
2810
|
return '--json';
|
|
4842
|
-
return undefined;
|
|
2811
|
+
return undefined;
|
|
4843
2812
|
}
|
|
4844
2813
|
const FULL_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4845
|
-
/** Width of `SessionMeta.shortId` / the hex a tmux alias embeds (`deriveShortId`). */
|
|
4846
2814
|
const SHORT_SESSION_ID_RE = /^[0-9a-f]{8}$/i;
|
|
4847
|
-
/**
|
|
4848
|
-
* A match unique enough to resolve on the FIRST peer that returns it and cancel
|
|
4849
|
-
* the rest of the fleet sweep: a full session UUID (globally unique), a live
|
|
4850
|
-
* tmux alias, or a bare 8-hex short id — all three name at most one session PER
|
|
4851
|
-
* ANSWERING peer, so the first reachable hit is enough (PHNX-3292: the alias/
|
|
4852
|
-
* short-id case is exactly what `agents tmux ls` prints, and it used to stay
|
|
4853
|
-
* all-settle right alongside a genuine keyword search).
|
|
4854
|
-
*
|
|
4855
|
-
* A session **label** is deliberately NOT definitive here even on an exact
|
|
4856
|
-
* match: labels are free-form and can collide, so a distinct session may carry
|
|
4857
|
-
* the same label on a peer that has not answered yet — early-exiting on the
|
|
4858
|
-
* first would silently, and nondeterministically, resume the wrong session
|
|
4859
|
-
* (whichever peer replied first). Labels therefore stay all-settle so a
|
|
4860
|
-
* cross-machine label conflict surfaces as an ambiguity, and a short-id PREFIX
|
|
4861
|
-
* *shorter* than the full 8-hex width stays all-settle for the same reason
|
|
4862
|
-
* (RUSH-2203).
|
|
4863
|
-
*
|
|
4864
|
-
* This is about cancelling a sweep that is still IN FLIGHT, where a peer that
|
|
4865
|
-
* has not answered yet is still expected to. It is deliberately stricter than
|
|
4866
|
-
* `isUniqueEnoughSelector`, which decides what to do once the sweep is OVER and
|
|
4867
|
-
* a peer has definitively not answered — see SES-9a. A collision between two
|
|
4868
|
-
* peers that BOTH answer before the abort lands is still caught by the ordinary
|
|
4869
|
-
* uniqueness gate in `metadataResolveOutcome`/`fleetCandidatesByQuery`; only a
|
|
4870
|
-
* peer that has not answered yet, at the moment of the FIRST hit, is the
|
|
4871
|
-
* accepted risk (documented in the PHNX-3292 plan).
|
|
4872
|
-
*/
|
|
4873
2815
|
export function isDefinitiveMatch(session, selector) {
|
|
4874
2816
|
const trimmed = selector.trim();
|
|
4875
2817
|
if (FULL_SESSION_ID_RE.test(trimmed)) {
|
|
@@ -4878,27 +2820,11 @@ export function isDefinitiveMatch(session, selector) {
|
|
|
4878
2820
|
const shortId = shortIdFromName(trimmed) ?? (SHORT_SESSION_ID_RE.test(trimmed) ? trimmed : undefined);
|
|
4879
2821
|
return !!shortId && session.shortId.toLowerCase() === shortId.toLowerCase();
|
|
4880
2822
|
}
|
|
4881
|
-
/**
|
|
4882
|
-
* Whether a selector may enable early-exit on the cancellable fan-out: a full
|
|
4883
|
-
* UUID, a tmux alias, or an exact 8-hex short id — every shape that is globally
|
|
4884
|
-
* unique enough for the first hit to be the only hit worth waiting for. Labels,
|
|
4885
|
-
* keywords, and short-id prefixes narrower than 8 hex stay all-settle: their
|
|
4886
|
-
* uniqueness (or conflict) is only knowable once every peer has answered. See
|
|
4887
|
-
* {@link isDefinitiveMatch}.
|
|
4888
|
-
*/
|
|
4889
2823
|
export function selectorAllowsEarlyExit(selector) {
|
|
4890
2824
|
const trimmed = selector.trim();
|
|
4891
2825
|
return FULL_SESSION_ID_RE.test(trimmed) || isAgentTmuxAlias(trimmed) || SHORT_SESSION_ID_RE.test(trimmed);
|
|
4892
2826
|
}
|
|
4893
|
-
/** Resolve a fleet sweep through the same canonical full-id / prefix resolver as
|
|
4894
|
-
* local lookups, then group copies by logical session id. Synced mirrors of one
|
|
4895
|
-
* session therefore stay one candidate even when several machines report them;
|
|
4896
|
-
* distinct ids sharing a prefix remain distinct ambiguity candidates. */
|
|
4897
2827
|
export function fleetCandidatesByQuery(rows, query, trustResolvedRows = false) {
|
|
4898
|
-
// An id selector must still be prefix-filtered defensively against older peers
|
|
4899
|
-
// returning content mentioners. Keyword rows, however, were already matched by
|
|
4900
|
-
// each peer's own FTS index; the parent does not own that transcript/index and
|
|
4901
|
-
// must not re-run metadata-only filtering that could discard a content hit.
|
|
4902
2828
|
const matched = !trustResolvedRows && looksLikeSessionId(query)
|
|
4903
2829
|
? resolveSessionQuery(rows, query, { indexFallback: false }).matches
|
|
4904
2830
|
: rows;
|
|
@@ -4906,7 +2832,7 @@ export function fleetCandidatesByQuery(rows, query, trustResolvedRows = false) {
|
|
|
4906
2832
|
for (const session of matched) {
|
|
4907
2833
|
const machine = session.machine;
|
|
4908
2834
|
if (!machine)
|
|
4909
|
-
continue;
|
|
2835
|
+
continue;
|
|
4910
2836
|
const logicalId = session.id.toLowerCase();
|
|
4911
2837
|
let byMachine = byId.get(logicalId);
|
|
4912
2838
|
if (!byMachine) {
|
|
@@ -4921,7 +2847,6 @@ export function fleetCandidatesByQuery(rows, query, trustResolvedRows = false) {
|
|
|
4921
2847
|
return { id: hits[0].session.id, hits };
|
|
4922
2848
|
});
|
|
4923
2849
|
}
|
|
4924
|
-
/** Resolve through the canonical metadata+content union used by keyword search. */
|
|
4925
2850
|
function resolveIndexedMetadataRows(indexed, selector, scope) {
|
|
4926
2851
|
const alias = resolveSessionAlias(selector);
|
|
4927
2852
|
if (alias.kind === 'resolved') {
|
|
@@ -4933,18 +2858,12 @@ function resolveIndexedMetadataRows(indexed, selector, scope) {
|
|
|
4933
2858
|
}
|
|
4934
2859
|
return resolveSessionQuery(indexed, selector, { indexFallback: false, scope }).matches;
|
|
4935
2860
|
}
|
|
4936
|
-
/**
|
|
4937
|
-
* ID-shaped selectors go straight to SQLite's primary-key/short-id lookup.
|
|
4938
|
-
* Keyword and label selectors still need the broader metadata pool.
|
|
4939
|
-
*/
|
|
4940
2861
|
function indexedRowsForSelector(selector, scope) {
|
|
4941
2862
|
const indexed = looksLikeSessionId(selector)
|
|
4942
2863
|
? findSessionsById(selector)
|
|
4943
2864
|
: querySessions();
|
|
4944
2865
|
return applyScopeFilters(indexed, scope);
|
|
4945
2866
|
}
|
|
4946
|
-
/** Fixed peer argv for the metadata resolver. Scope flags compose identically on
|
|
4947
|
-
* every host; `--all` removes the SSH login cwd/time window, not agent/project filters. */
|
|
4948
2867
|
export function metadataResolveForwardedArgs(selector, scope) {
|
|
4949
2868
|
const args = ['sessions', '--resolve-safe-v1', selector, '--json', '--all', '--local'];
|
|
4950
2869
|
if (scope.agent)
|
|
@@ -4953,7 +2872,6 @@ export function metadataResolveForwardedArgs(selector, scope) {
|
|
|
4953
2872
|
args.push('--project', scope.project);
|
|
4954
2873
|
return args;
|
|
4955
2874
|
}
|
|
4956
|
-
/** Build the local-only peer argv for a distributed tool search. */
|
|
4957
2875
|
export function toolSearchForwardedArgs(argv, hosts) {
|
|
4958
2876
|
const args = ensureWholeIndex(buildForwardedArgs(argv, new Set(hosts)).filter((arg) => arg !== '--fleet'));
|
|
4959
2877
|
if (!args.includes('--json'))
|
|
@@ -4962,19 +2880,7 @@ export function toolSearchForwardedArgs(argv, hosts) {
|
|
|
4962
2880
|
args.push('--local');
|
|
4963
2881
|
return args;
|
|
4964
2882
|
}
|
|
4965
|
-
/** Width of the short id the CLI prints everywhere (`SessionMeta.shortId`):
|
|
4966
|
-
* 8 hex chars / 32 bits. A selector shorter than this is treated as a keyword,
|
|
4967
|
-
* not an identifier. */
|
|
4968
2883
|
const SHORT_SESSION_ID_WIDTH = 8;
|
|
4969
|
-
/**
|
|
4970
|
-
* Whether a selector names ONE session precisely enough to resolve from the
|
|
4971
|
-
* reachable fleet alone when some peer did not answer (SES-9a).
|
|
4972
|
-
*
|
|
4973
|
-
* Deliberately stricter than `looksLikeSessionId`, which accepts any 6+ char
|
|
4974
|
-
* `[0-9a-f-]` run and so matches ordinary words (`facade`, `decade`, `beaded`).
|
|
4975
|
-
* Those are keywords a user typed as a search, not identifiers, and they must
|
|
4976
|
-
* keep waiting for every peer.
|
|
4977
|
-
*/
|
|
4978
2884
|
export function isUniqueEnoughSelector(selector) {
|
|
4979
2885
|
const trimmed = selector.trim();
|
|
4980
2886
|
if (isCompleteSessionId(trimmed))
|
|
@@ -4982,35 +2888,8 @@ export function isUniqueEnoughSelector(selector) {
|
|
|
4982
2888
|
return /^[0-9a-f-]+$/i.test(trimmed)
|
|
4983
2889
|
&& trimmed.replace(/-/g, '').length >= SHORT_SESSION_ID_WIDTH;
|
|
4984
2890
|
}
|
|
4985
|
-
/** Resolution fails closed for keywords and labels when a peer did not answer.
|
|
4986
|
-
* An id-shaped selector at least `SHORT_SESSION_ID_WIDTH` hex chars wide (or a
|
|
4987
|
-
* complete id) resolves from a single reachable hit even when an unrelated
|
|
4988
|
-
* registered device is offline — see SES-9a for the accepted collision risk. */
|
|
4989
2891
|
export function metadataResolveOutcome(localMatches, remote, selector) {
|
|
4990
|
-
// Every peer answered through --resolve-safe-v1, so its native-id row is
|
|
4991
|
-
// already the result of resolving the original selector. Re-filtering by the
|
|
4992
|
-
// selector would discard alias suffixes such as c1f3d813 because the native
|
|
4993
|
-
// UUID intentionally has a different prefix.
|
|
4994
2892
|
const candidates = fleetCandidatesByQuery([...localMatches, ...remote.sessions], selector, true);
|
|
4995
|
-
// One hit on the REACHABLE fleet resolves a precise-enough id selector even
|
|
4996
|
-
// when a peer is offline (SES-9a). This is a deliberate, bounded trade, not a
|
|
4997
|
-
// claim that prefixes cannot collide — they can, which is why the `ambiguous`
|
|
4998
|
-
// branch below still exists for peers that DID answer:
|
|
4999
|
-
// - refusing costs 100% of lookups on any fleet with a permanently offline
|
|
5000
|
-
// device (9 of them here), to avoid a prefix collision that ALSO has to
|
|
5001
|
-
// land on the one box that did not answer. Rate depends on the id type:
|
|
5002
|
-
// a UUIDv4 short id is unique in practice, while a time-ordered
|
|
5003
|
-
// UUIDv7/ULID prefix is largely a timestamp and collides much more
|
|
5004
|
-
// readily (see deriveShortId in session/db.ts);
|
|
5005
|
-
// - `isUniqueEnoughSelector` keeps this off keyword-shaped queries, so a
|
|
5006
|
-
// 6-char word like `facade` or `decade` is not treated as an identifier.
|
|
5007
|
-
// A label stays fail-closed at any length: labels are free-form and collide by
|
|
5008
|
-
// design, so one hit there really is a guess.
|
|
5009
|
-
// (RUSH-2203's early-exit — see isDefinitiveMatch — cancels the sweep
|
|
5010
|
-
// mid-flight, where a peer that has not answered YET is still expected to
|
|
5011
|
-
// answer, so it stays narrower than this post-sweep gate even though
|
|
5012
|
-
// PHNX-3292 widened it past UUID to alias/8-hex too. Here the sweep is
|
|
5013
|
-
// already over.)
|
|
5014
2893
|
if (isUniqueEnoughSelector(selector) && candidates.length === 1) {
|
|
5015
2894
|
return { kind: 'resolved', session: candidates[0].hits[0].session };
|
|
5016
2895
|
}
|
|
@@ -5022,74 +2901,17 @@ export function metadataResolveOutcome(localMatches, remote, selector) {
|
|
|
5022
2901
|
return { kind: 'ambiguous', candidates };
|
|
5023
2902
|
return { kind: 'resolved', session: candidates[0].hits[0].session };
|
|
5024
2903
|
}
|
|
5025
|
-
/**
|
|
5026
|
-
* Whether a local candidate answers a full-UUID selector **definitively** — i.e.
|
|
5027
|
-
* whether finding it on this box is the whole answer, so the fleet fan-out can be
|
|
5028
|
-
* skipped (PHNX-3890).
|
|
5029
|
-
*
|
|
5030
|
-
* Two shapes qualify, and both are claims this box can actually back:
|
|
5031
|
-
*
|
|
5032
|
-
* - **A real transcript on this disk** (`filePath`). It renders locally, whoever
|
|
5033
|
-
* owns it — a local session or a fleet-synced mirror — so no SSH hop is needed.
|
|
5034
|
-
* - **A genuine peer attribution** (`machine` names another box). The row already
|
|
5035
|
-
* routes the read to its owner through `transcriptOnPeerOf`, so the peer that
|
|
5036
|
-
* would answer the fan-out is the peer the render already dials.
|
|
5037
|
-
*
|
|
5038
|
-
* What does NOT qualify is the launcher-shim shape: a transcript-less live row
|
|
5039
|
-
* whose `machine` is this box only because it DEFAULTED there
|
|
5040
|
-
* (`activeSessionToSessionMeta`'s `active.machine ?? self`,
|
|
5041
|
-
* `computeLocalMetadataMatches`'s `machine || localMachine`). A dispatcher holds
|
|
5042
|
-
* exactly that row for a session whose agent and transcript live on a peer: the
|
|
5043
|
-
* launch process is here, the conversation is not. Treating it as definitive is
|
|
5044
|
-
* what dead-ended `agents sessions preview <full-uuid>` on the local
|
|
5045
|
-
* "Live session — full transcript not indexed here." stub while a passive peer —
|
|
5046
|
-
* which has no local row at all, so it fans out — rendered the real digest.
|
|
5047
|
-
* Process locality is not transcript locality.
|
|
5048
|
-
*/
|
|
5049
2904
|
export function isLocallyDefinitiveMatch(session, self) {
|
|
5050
2905
|
if (session.filePath)
|
|
5051
2906
|
return true;
|
|
5052
2907
|
return !!session.machine && session.machine !== self;
|
|
5053
2908
|
}
|
|
5054
|
-
/**
|
|
5055
|
-
* Drop the launcher-shim local rows that a peer has since answered for
|
|
5056
|
-
* (PHNX-3890). `fleetCandidatesByQuery` groups a logical session's copies per
|
|
5057
|
-
* machine and every consumer reads `hits[0]`, with local rows passed first — so
|
|
5058
|
-
* simply fanning out is not enough: the self-defaulted shim would still win the
|
|
5059
|
-
* attribution and route the read back to a box with no transcript. When the peer
|
|
5060
|
-
* that actually owns the session has answered the sweep, its row is the strictly
|
|
5061
|
-
* better one, and the shim carries no information the candidate loses (same
|
|
5062
|
-
* logical id, so the candidate — and any ambiguity it is part of — survives
|
|
5063
|
-
* through the remote hit).
|
|
5064
|
-
*
|
|
5065
|
-
* A row that is {@link isLocallyDefinitiveMatch} is never dropped, so a locally
|
|
5066
|
-
* readable transcript or a synced mirror still renders here with no SSH hop. When
|
|
5067
|
-
* no peer answered for that id, the shim is kept: with no fleet evidence, a
|
|
5068
|
-
* transcript-less self-attributed row is indistinguishable from a session THIS box
|
|
5069
|
-
* just started, and dropping it would re-break the cold-index lookup RUSH-2682
|
|
5070
|
-
* fixed.
|
|
5071
|
-
*/
|
|
5072
2909
|
export function preferOwnerAttribution(localMatches, remoteSessions, self) {
|
|
5073
2910
|
if (remoteSessions.length === 0)
|
|
5074
2911
|
return localMatches;
|
|
5075
2912
|
const answeredByPeer = new Set(remoteSessions.map(session => session.id.toLowerCase()));
|
|
5076
2913
|
return localMatches.filter(session => isLocallyDefinitiveMatch(session, self) || !answeredByPeer.has(session.id.toLowerCase()));
|
|
5077
2914
|
}
|
|
5078
|
-
/**
|
|
5079
|
-
* Local metadata candidates for a selector: the indexed SQLite rows, plus — when
|
|
5080
|
-
* an id-shaped selector misses the index entirely — the live-session registry
|
|
5081
|
-
* (RUSH-2682).
|
|
5082
|
-
*
|
|
5083
|
-
* Indexing is lazy (it only runs inside `discoverSessions`), so a session THIS
|
|
5084
|
-
* box just started is "running" in `agents sessions --active` minutes before its
|
|
5085
|
-
* transcript reaches the local index. Every id resolver (`preview`, `resume`,
|
|
5086
|
-
* `focus`, and the fan-out peer that answers `--resolve-safe-v1`) therefore
|
|
5087
|
-
* consults the SAME live registry `--active` reads before declaring a running
|
|
5088
|
-
* session non-existent. It reshapes process state the registry already computed;
|
|
5089
|
-
* it parses no transcript and renders nothing. The live path is entered ONLY on a
|
|
5090
|
-
* cold id miss, so an indexed hit keeps its richer row and keyword resolution is
|
|
5091
|
-
* unchanged.
|
|
5092
|
-
*/
|
|
5093
2915
|
export async function computeLocalMetadataMatches(selector, scope, deps = {}) {
|
|
5094
2916
|
const localMachine = machineId();
|
|
5095
2917
|
const includeLocal = !scope.hosts?.length || shouldIncludeLocal(scope.hosts, localMachine);
|
|
@@ -5126,22 +2948,9 @@ export async function computeLocalMetadataMatches(selector, scope, deps = {}) {
|
|
|
5126
2948
|
}
|
|
5127
2949
|
return hydrated;
|
|
5128
2950
|
}
|
|
5129
|
-
/**
|
|
5130
|
-
* Resolve an id-shaped selector against the local live-session registry
|
|
5131
|
-
* (RUSH-2682). Reads the daemon-warmed active snapshot first (cheap); if that
|
|
5132
|
-
* snapshot predates a just-started session and misses, it forces ONE fresh
|
|
5133
|
-
* gather before conceding, so a session started seconds ago resolves
|
|
5134
|
-
* synchronously. Never throws — a degraded registry read yields no candidates,
|
|
5135
|
-
* so the caller falls through to the ordinary not-found path.
|
|
5136
|
-
*/
|
|
5137
2951
|
export async function liveMetadataMatches(selector, scope, self, deps = {}) {
|
|
5138
2952
|
const load = deps.loadActive ?? loadLocalActiveSessions;
|
|
5139
2953
|
const loadFleet = deps.loadFleetActive ?? (() => readActiveSessionsCache('fleet')?.sessions ?? []);
|
|
5140
|
-
// A launcher-shim row whose `machine` self-defaulted to this box gets its true
|
|
5141
|
-
// EXECUTION host from the fleet snapshot, so a session dispatched to a peer is
|
|
5142
|
-
// read on that peer instead of dead-ending on the local stub (PHNX-3890). The
|
|
5143
|
-
// read is best-effort: an absent/cold snapshot leaves the row untouched and the
|
|
5144
|
-
// fleet fan-out in resolveSessionMetadataValue supplies the owner instead.
|
|
5145
2954
|
let fleetExecMachine;
|
|
5146
2955
|
try {
|
|
5147
2956
|
fleetExecMachine = fleetExecutionMachineById(loadFleet());
|
|
@@ -5162,23 +2971,9 @@ export async function liveMetadataMatches(selector, scope, self, deps = {}) {
|
|
|
5162
2971
|
return [];
|
|
5163
2972
|
}
|
|
5164
2973
|
}
|
|
5165
|
-
/** Reusable local-first resolver for `agents run --resume` and `agents sessions resume`.
|
|
5166
|
-
* Full UUIDs hit the local SQLite index without any SSH fan-out. */
|
|
5167
2974
|
export async function resolveSessionMetadataValue(selector, scope = {}, deps = { gatherRemoteList }) {
|
|
5168
2975
|
const localMatches = await computeLocalMetadataMatches(selector, scope, deps);
|
|
5169
2976
|
const localMachine = machineId();
|
|
5170
|
-
// A full-UUID local hit resolves with ZERO SSH: a UUID is globally unique, so
|
|
5171
|
-
// finding it locally is the whole answer and no peer is dialed. (A label is
|
|
5172
|
-
// NOT resolved local-only here — it can collide with a same-label session on a
|
|
5173
|
-
// peer, so it must consult the fleet; see isDefinitiveMatch, RUSH-2203.) The
|
|
5174
|
-
// local index lookup is synchronous and completes before any fan-out spawns,
|
|
5175
|
-
// so this local hit never waits on a peer.
|
|
5176
|
-
//
|
|
5177
|
-
// "Found it locally" must mean this box can actually ANSWER for it, though —
|
|
5178
|
-
// see isLocallyDefinitiveMatch. A transcript-less launcher shim for a session
|
|
5179
|
-
// executing on a peer is a local row that is not a local answer, and
|
|
5180
|
-
// short-circuiting on it skipped the one fan-out that could have named the
|
|
5181
|
-
// owner (PHNX-3890).
|
|
5182
2977
|
if (FULL_SESSION_ID_RE.test(selector)) {
|
|
5183
2978
|
const localOutcome = metadataResolveOutcome(localMatches, { sessions: [], unreachable: [] }, selector);
|
|
5184
2979
|
if (localOutcome.kind === 'resolved' && isLocallyDefinitiveMatch(localOutcome.session, localMachine)) {
|
|
@@ -5189,10 +2984,6 @@ export async function resolveSessionMetadataValue(selector, scope = {}, deps = {
|
|
|
5189
2984
|
return metadataResolveOutcome(localMatches, { sessions: [], unreachable: [] }, selector);
|
|
5190
2985
|
try {
|
|
5191
2986
|
const forwarded = metadataResolveForwardedArgs(selector, scope);
|
|
5192
|
-
// Definitive lookups (full UUID / exact label) opt into early-exit: the
|
|
5193
|
-
// first peer holding the match resolves the sweep and SIGTERMs the rest, so
|
|
5194
|
-
// a fast peer's hit is not bounded by the slowest/unreachable peer. Short-id
|
|
5195
|
-
// prefixes stay all-settle (ambiguity is only known once every peer answers).
|
|
5196
2987
|
const remote = await deps.gatherRemoteList(forwarded, scope.hosts, selectorAllowsEarlyExit(selector)
|
|
5197
2988
|
? { isDefinitive: (session) => isDefinitiveMatch(session, selector) }
|
|
5198
2989
|
: undefined);
|
|
@@ -5202,30 +2993,14 @@ export async function resolveSessionMetadataValue(selector, scope = {}, deps = {
|
|
|
5202
2993
|
return metadataResolveOutcome(localMatches, { sessions: [], unreachable: [error?.message ?? 'fleet fan-out'] }, selector);
|
|
5203
2994
|
}
|
|
5204
2995
|
}
|
|
5205
|
-
/** Resolve one selector to indexed metadata across the fleet without reading or
|
|
5206
|
-
* rendering transcript events. A peer answering the parent sweep returns every
|
|
5207
|
-
* local candidate; the parent performs the one logical-session uniqueness gate.
|
|
5208
|
-
* Backs `agents sessions --resolve <selector> --json`. Exported (deps already
|
|
5209
|
-
* injectable) so tests can drive this real call site against a fake fan-out
|
|
5210
|
-
* instead of only the pure resolver it wraps. */
|
|
5211
2996
|
export async function resolveSessionMetadata(selector, scope, deps = { gatherRemoteList }) {
|
|
5212
|
-
// Same local candidate set as resolveSessionMetadataValue, so the peer
|
|
5213
|
-
// answering a fan-out (`--resolve-safe-v1`, NO_FANOUT) surfaces a running
|
|
5214
|
-
// session its own index has not caught yet — the cross-device half of the
|
|
5215
|
-
// RUSH-2682 fix, since the parent's fan-out reads this box's answer.
|
|
5216
2997
|
const localMatches = await computeLocalMetadataMatches(selector, scope, deps);
|
|
5217
|
-
// A peer is already inside gatherRemoteList. Return all local candidates so
|
|
5218
|
-
// the parent can distinguish a fleet-wide unique match from an ambiguity.
|
|
5219
2998
|
if (process.env[NO_FANOUT_ENV] === '1') {
|
|
5220
2999
|
process.stdout.write(serializeResolvedSessionsJson(localMatches));
|
|
5221
3000
|
return;
|
|
5222
3001
|
}
|
|
5223
3002
|
const outcome = await resolveSessionMetadataValue(selector, scope, deps);
|
|
5224
3003
|
if (outcome.kind === 'partial') {
|
|
5225
|
-
// RUSH-2492: an unreachable peer is a warning, not a hard failure. The
|
|
5226
|
-
// resolver already resolves an id found on the reachable fleet (SES-9a),
|
|
5227
|
-
// so reaching here means the session was not found on any device we
|
|
5228
|
-
// COULD reach — it may live on an unreachable peer we could not check.
|
|
5229
3004
|
const offline = outcome.failedPeers;
|
|
5230
3005
|
console.error(chalk.yellow(`Warning: ${offline.length} device(s) unreachable, not checked: ${offline.join(', ')}`));
|
|
5231
3006
|
console.error(chalk.red(`No session matching "${selector}" on any reachable device (${offline.length} unreachable, not checked).`));
|
|
@@ -5254,11 +3029,6 @@ async function resolveSessionAcrossFleet(query, mode, hosts, deps = { gatherRemo
|
|
|
5254
3029
|
let deviceCount = 0;
|
|
5255
3030
|
let unreachable = [];
|
|
5256
3031
|
try {
|
|
5257
|
-
// Force whole-index scope (--all): the peer runs in its SSH-login home dir,
|
|
5258
|
-
// whose cwd would otherwise silently narrow the lookup and hide the row.
|
|
5259
|
-
// --json so each peer answers a parseable array; --local so it answers for
|
|
5260
|
-
// itself and never re-fans-out (belt-and-suspenders with the parent's
|
|
5261
|
-
// AGENTS_SESSIONS_LOCAL, which remote-list also sets on the peer).
|
|
5262
3032
|
const forwarded = ['sessions', query, '--json', '--all', '--local'];
|
|
5263
3033
|
const remote = await deps.gatherRemoteList(forwarded, hosts, selectorAllowsEarlyExit(query)
|
|
5264
3034
|
? { isDefinitive: (session) => isDefinitiveMatch(session, query) }
|
|
@@ -5268,8 +3038,6 @@ async function resolveSessionAcrossFleet(query, mode, hosts, deps = { gatherRemo
|
|
|
5268
3038
|
unreachable = remote.unreachable;
|
|
5269
3039
|
}
|
|
5270
3040
|
catch {
|
|
5271
|
-
// A fan-out failure is not an exact resolution — treat as not-found so the
|
|
5272
|
-
// caller prints the honest message rather than a half-answer.
|
|
5273
3041
|
candidates = [];
|
|
5274
3042
|
}
|
|
5275
3043
|
finally {
|
|
@@ -5290,10 +3058,6 @@ async function resolveSessionAcrossFleet(query, mode, hosts, deps = { gatherRemo
|
|
|
5290
3058
|
}
|
|
5291
3059
|
const candidate = candidates[0];
|
|
5292
3060
|
const { machine } = candidate.hits[0];
|
|
5293
|
-
// Render the remote summary by re-running `sessions <id>` ON the peer. --local
|
|
5294
|
-
// keeps that render on the peer (it owns the transcript); the mode flag matches
|
|
5295
|
-
// the mode the user asked for. No TTY: a summary/markdown/json render is a
|
|
5296
|
-
// one-shot capture, not an interactive resume.
|
|
5297
3061
|
const peerArgs = ['sessions', candidate.id, '--local'];
|
|
5298
3062
|
const flag = modeFlag(mode);
|
|
5299
3063
|
if (flag)
|
|
@@ -5302,11 +3066,10 @@ async function resolveSessionAcrossFleet(query, mode, hosts, deps = { gatherRemo
|
|
|
5302
3066
|
if (result === 'no-target') {
|
|
5303
3067
|
console.error(chalk.red(`Session ${candidate.id} is on ${machine}, but it is not a reachable registered device.`));
|
|
5304
3068
|
console.error(chalk.gray('Register it with `agents devices` or run the command on that machine.'));
|
|
5305
|
-
return { kind: 'conflict' };
|
|
3069
|
+
return { kind: 'conflict' };
|
|
5306
3070
|
}
|
|
5307
3071
|
return { kind: 'rendered' };
|
|
5308
3072
|
}
|
|
5309
|
-
/** Register the `agents sessions` command with all its options and help text. */
|
|
5310
3073
|
export function registerSessionsCommands(program) {
|
|
5311
3074
|
const sessionsCmd = program
|
|
5312
3075
|
.command('sessions')
|
|
@@ -5480,15 +3243,10 @@ export function registerSessionsCommands(program) {
|
|
|
5480
3243
|
});
|
|
5481
3244
|
sessionsCmd.action(async (query, options, command) => {
|
|
5482
3245
|
if (options.browser) {
|
|
5483
|
-
// Alias for `agents browser sessions`: a profile positional narrows to one
|
|
5484
|
-
// profile. `--no-interactive` is already a top-level sessionsCmd option
|
|
5485
|
-
// (options.interactive), so it carries through for free.
|
|
5486
3246
|
await runBrowserSessionsCommand({ profile: query, json: options.json, interactive: options.interactive });
|
|
5487
3247
|
return;
|
|
5488
3248
|
}
|
|
5489
3249
|
if (options.computer) {
|
|
5490
|
-
// Alias for `agents computer sessions`: a machine positional narrows to
|
|
5491
|
-
// one machine. `--no-interactive` carries through the same way.
|
|
5492
3250
|
const limit = options.limit === undefined ? undefined : Number.parseInt(options.limit, 10);
|
|
5493
3251
|
const deviceArgs = [...(options.device ?? []), ...(options.devices ?? [])];
|
|
5494
3252
|
const allFleet = deviceArgs.some((host) => ['all', 'fleet'].includes(host.toLowerCase()));
|
|
@@ -5668,13 +3426,11 @@ function findClaudeSessionsInProject(sessions, historyEntry) {
|
|
|
5668
3426
|
function findClaudeProjectSessions(sessions, historyEntry) {
|
|
5669
3427
|
if (!historyEntry.project)
|
|
5670
3428
|
return [];
|
|
5671
|
-
// Resolve symlinks (e.g. macOS /var -> /private/var) so we match sessions
|
|
5672
|
-
// whose cwd was canonicalized at scan time.
|
|
5673
3429
|
let projectRoot = historyEntry.project;
|
|
5674
3430
|
try {
|
|
5675
3431
|
projectRoot = fs.realpathSync(projectRoot);
|
|
5676
3432
|
}
|
|
5677
|
-
catch {
|
|
3433
|
+
catch { }
|
|
5678
3434
|
return sessions.filter(session => session.agent === 'claude' &&
|
|
5679
3435
|
typeof session.cwd === 'string' &&
|
|
5680
3436
|
isWithinProject(session.cwd, projectRoot));
|
|
@@ -5742,8 +3498,6 @@ function findClaudeResumeTimestamp(filePath, targetTimestampMs) {
|
|
|
5742
3498
|
}
|
|
5743
3499
|
}
|
|
5744
3500
|
function isWithinProject(sessionCwd, projectRoot) {
|
|
5745
|
-
// Compare separator- and case-normalized (Windows folds `\`→`/` and lowercases)
|
|
5746
|
-
// so a backslash session cwd matches a forward-slash project root and vice versa.
|
|
5747
3501
|
const cwd = toComparablePath(sessionCwd);
|
|
5748
3502
|
const root = toComparablePath(projectRoot);
|
|
5749
3503
|
return cwd === root || cwd.startsWith(root + '/');
|
|
@@ -5756,9 +3510,6 @@ function sessionDistance(session, historyEntry) {
|
|
|
5756
3510
|
return Number.MAX_SAFE_INTEGER;
|
|
5757
3511
|
return Math.abs(sessionTime - historyEntry.timestampMs);
|
|
5758
3512
|
}
|
|
5759
|
-
// ---------------------------------------------------------------------------
|
|
5760
|
-
// Formatting helpers
|
|
5761
|
-
// ---------------------------------------------------------------------------
|
|
5762
3513
|
function formatAbsoluteTime(isoTimestamp) {
|
|
5763
3514
|
const d = new Date(isoTimestamp);
|
|
5764
3515
|
if (isNaN(d.getTime()))
|