@phnx-labs/agents-cli 1.22.66 → 1.22.69
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 +56 -0
- package/README.md +21 -8
- package/dist/bootstrap.js +6 -3
- package/dist/commands/browser.js +46 -21
- package/dist/commands/daemon-test-harness.d.ts +1 -1
- package/dist/commands/daemon-test-harness.js +2 -2
- package/dist/commands/daemon.js +21 -22
- package/dist/commands/exec.js +50 -0
- package/dist/commands/feed.js +20 -7
- package/dist/commands/monitors.js +5 -2
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- package/dist/commands/repo.js +57 -19
- package/dist/commands/resume.d.ts +16 -0
- package/dist/commands/resume.js +41 -8
- package/dist/commands/routines.js +42 -21
- package/dist/commands/send.js +29 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-optimize.js +1 -1
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/sessions.js +4 -11
- package/dist/commands/share.d.ts +5 -3
- package/dist/commands/share.js +73 -20
- package/dist/commands/ssh.js +205 -2
- package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
- package/dist/lib/accounting/account-pool-collect.js +6 -4
- package/dist/lib/accounting/usage-ingest.js +4 -2
- package/dist/lib/accounting/usage-sync.d.ts +38 -93
- package/dist/lib/accounting/usage-sync.js +66 -210
- package/dist/lib/accounting/usage.d.ts +18 -5
- package/dist/lib/accounting/usage.js +165 -20
- package/dist/lib/auth-health.d.ts +8 -0
- package/dist/lib/auth-health.js +4 -4
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- package/dist/lib/browser/caller-identity.d.ts +12 -0
- package/dist/lib/browser/caller-identity.js +19 -0
- package/dist/lib/browser/ipc.d.ts +37 -32
- package/dist/lib/browser/ipc.js +146 -94
- package/dist/lib/browser/task-index.d.ts +10 -2
- package/dist/lib/browser/task-index.js +22 -3
- package/dist/lib/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/claude-account-token.js +108 -4
- package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
- package/dist/lib/daemon/account-state-daemon-service.js +81 -18
- package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
- package/dist/lib/daemon/auth-sync-service.js +17 -6
- package/dist/lib/daemon/catchup-service.d.ts +51 -0
- package/dist/lib/daemon/catchup-service.js +51 -0
- package/dist/lib/daemon/daemon.d.ts +12 -22
- package/dist/lib/daemon/daemon.js +463 -176
- package/dist/lib/daemon/runner.js +2 -0
- package/dist/lib/daemon/service.d.ts +22 -4
- package/dist/lib/daemon/service.js +2 -2
- package/dist/lib/daemon/supervisor.d.ts +55 -15
- package/dist/lib/daemon/supervisor.js +119 -29
- package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
- package/dist/lib/daemon/usage-sync-service.js +22 -18
- package/dist/lib/daemon-health.js +36 -31
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +12 -2
- package/dist/lib/daemon-ticks.d.ts +9 -6
- package/dist/lib/daemon-ticks.js +14 -8
- package/dist/lib/devices/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.js +15 -0
- package/dist/lib/feed/watch.d.ts +3 -0
- package/dist/lib/feed/watch.js +13 -3
- package/dist/lib/feed-broadcast.d.ts +64 -5
- package/dist/lib/feed-broadcast.js +124 -22
- package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
- package/dist/lib/fleet-shared-repo-sync.js +333 -0
- package/dist/lib/fleet-shared-state.d.ts +38 -0
- package/dist/lib/fleet-shared-state.js +105 -0
- package/dist/lib/hosts/remote-cmd.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.js +12 -3
- package/dist/lib/lock-compromise.d.ts +8 -0
- package/dist/lib/lock-compromise.js +12 -0
- package/dist/lib/monitors/engine.d.ts +2 -1
- package/dist/lib/monitors/engine.js +27 -2
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/owner-message.d.ts +12 -0
- package/dist/lib/owner-message.js +44 -0
- package/dist/lib/refresh-coordinator.js +2 -0
- package/dist/lib/run-trace-sync.d.ts +28 -0
- package/dist/lib/run-trace-sync.js +99 -0
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- package/dist/lib/secrets/push.d.ts +10 -0
- package/dist/lib/secrets/push.js +86 -7
- package/dist/lib/secrets/remote.d.ts +18 -6
- package/dist/lib/secrets/remote.js +29 -4
- package/dist/lib/secrets/reserved-sync.d.ts +28 -27
- package/dist/lib/secrets/reserved-sync.js +119 -101
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +5 -0
- package/dist/lib/session/actor-sidecar.d.ts +12 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +16 -2
- package/dist/lib/session/db.js +73 -22
- package/dist/lib/session/discover.d.ts +12 -3
- package/dist/lib/session/discover.js +187 -26
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +17 -0
- package/dist/lib/session/prompt.js +35 -0
- package/dist/lib/session/recovery.d.ts +43 -6
- package/dist/lib/session/recovery.js +80 -10
- package/dist/lib/session/remote/remote-list.d.ts +17 -1
- package/dist/lib/session/remote/remote-list.js +29 -4
- package/dist/lib/session/remote/watch.d.ts +25 -2
- package/dist/lib/session/remote/watch.js +188 -11
- package/dist/lib/session/session-cache.d.ts +2 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +67 -11
- package/dist/lib/share/publish.js +98 -16
- package/dist/lib/share/worker-template.js +105 -9
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/ssh-exec.d.ts +2 -0
- package/dist/lib/ssh-exec.js +20 -4
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
- package/dist/lib/testdata/daemon-health-writer.js +8 -0
- package/dist/lib/traces/backend.js +13 -2
- package/dist/lib/traces/sync.d.ts +7 -0
- package/dist/lib/traces/sync.js +9 -0
- package/dist/lib/usage-refresh.d.ts +8 -2
- package/dist/lib/usage-refresh.js +3 -3
- package/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
- package/dist/lib/account-state-service.d.ts +0 -21
- package/dist/lib/account-state-service.js +0 -60
|
@@ -1,129 +1,147 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fleet sync of the reserved file-backed `auth` bundle (PHNX-2371).
|
|
2
|
+
* Fleet sync of the reserved file-backed `auth` bundle (PHNX-2371/PHNX-3609).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Double-fire: each device's daemon only writes that device's own store (the
|
|
11
|
-
* destination). Two boxes that both have `auth` skip each other (already
|
|
12
|
-
* present). A box without `auth` is a pull target for a provisioned peer.
|
|
13
|
-
*
|
|
14
|
-
* The planner is pure so tests cover every skip/push branch with no SSH.
|
|
4
|
+
* Every daemon publishes only a safe auth readiness verdict into its owned
|
|
5
|
+
* fleet-shared state file. A deterministic ready publisher reads those verdicts
|
|
6
|
+
* and transfers the actual bundle only to a peer reporting `missing`. Secret
|
|
7
|
+
* values never enter Git; the exceptional provisioning transfer remains SSH,
|
|
8
|
+
* now async and kill-bounded instead of blocking the daemon event loop.
|
|
15
9
|
*/
|
|
16
10
|
import { AUTH_BUNDLE_NAME, inspectReservedAuthBundle } from './bundles.js';
|
|
17
|
-
import {
|
|
18
|
-
import { loadDevicesSync } from '../devices/registry.js';
|
|
11
|
+
import { pushBundleToHostAsync } from './push.js';
|
|
12
|
+
import { isDialableDevice, loadDevicesSync } from '../devices/registry.js';
|
|
19
13
|
import { sshTargetFor } from '../devices/connect.js';
|
|
20
14
|
import { isHostPinned, isDevicePinned, managedKnownHostsPath } from '../devices/known-hosts.js';
|
|
21
15
|
import { machineId, normalizeHost } from '../session/sync/config.js';
|
|
22
|
-
import {
|
|
23
|
-
|
|
16
|
+
import { readFleetSharedDeviceStates, updateFleetSharedDeviceState, } from '../fleet-shared-state.js';
|
|
17
|
+
import { getUserAgentsDir } from '../state.js';
|
|
18
|
+
/** Each import/read-back SSH operation gets this deadline plus the SSH hard-kill grace. */
|
|
19
|
+
export const AUTH_SYNC_PUSH_DEADLINE_MS = 20_000;
|
|
20
|
+
export function planAuthBundlePush(localAuthOk, localIsPublisher, devices) {
|
|
24
21
|
if (!localAuthOk) {
|
|
25
|
-
return devices.map((
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}));
|
|
22
|
+
return devices.map((device) => ({ action: 'skip', device: device.name, reason: 'no local file-backed auth bundle' }));
|
|
23
|
+
}
|
|
24
|
+
if (!localIsPublisher) {
|
|
25
|
+
return devices.map((device) => ({ action: 'skip', device: device.name, reason: 'another ready device is the elected auth publisher' }));
|
|
30
26
|
}
|
|
31
|
-
return devices.map((
|
|
32
|
-
if (!
|
|
33
|
-
return { action: 'skip', device:
|
|
34
|
-
if (!
|
|
35
|
-
return {
|
|
36
|
-
action: 'skip',
|
|
37
|
-
device: d.name,
|
|
38
|
-
reason: `host key not pinned; run \`agents ssh ${d.name}\` once`,
|
|
39
|
-
};
|
|
27
|
+
return devices.map((device) => {
|
|
28
|
+
if (!device.reachable)
|
|
29
|
+
return { action: 'skip', device: device.name, reason: 'unreachable' };
|
|
30
|
+
if (!device.pinned) {
|
|
31
|
+
return { action: 'skip', device: device.name, reason: `host key not pinned; run \`agents ssh ${device.name}\` once` };
|
|
40
32
|
}
|
|
41
|
-
if (
|
|
42
|
-
return { action: 'skip', device:
|
|
43
|
-
|
|
33
|
+
if (device.remoteAuth === 'ready')
|
|
34
|
+
return { action: 'skip', device: device.name, reason: 'already present' };
|
|
35
|
+
if (device.remoteAuth === 'invalid')
|
|
36
|
+
return { action: 'skip', device: device.name, reason: 'remote auth bundle uses the wrong backend' };
|
|
37
|
+
if (device.remoteAuth === 'unknown') {
|
|
38
|
+
return { action: 'skip', device: device.name, reason: 'no shared auth verdict has arrived from this peer' };
|
|
39
|
+
}
|
|
40
|
+
return { action: 'push', device: device.name };
|
|
44
41
|
});
|
|
45
42
|
}
|
|
46
|
-
/**
|
|
47
|
-
* Map a `fleet apply` device probe onto the auth-sync skip/push inputs.
|
|
48
|
-
*
|
|
49
|
-
* Presence is an own-property check on `remoteBundles` (same gate as
|
|
50
|
-
* `decideSecretPush`): a missing listing, a parse failure, or a prototype
|
|
51
|
-
* name must all mean "not present, so push" — never "present, so skip".
|
|
52
|
-
*/
|
|
53
|
-
export function authPresenceFromProbe(probe) {
|
|
54
|
-
return {
|
|
55
|
-
reachable: probe.reachable,
|
|
56
|
-
remoteHasAuth: !!(probe.remoteBundles &&
|
|
57
|
-
Object.prototype.hasOwnProperty.call(probe.remoteBundles, AUTH_BUNDLE_NAME)),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
/** Production default: live SSH probe of reachability + `secrets list --json`. */
|
|
61
|
-
export function defaultAuthSyncProbe(device) {
|
|
62
|
-
return authPresenceFromProbe(probeDevice(device, { withSecrets: true }));
|
|
63
|
-
}
|
|
64
43
|
function defaultDevices() {
|
|
65
44
|
return Object.values(loadDevicesSync());
|
|
66
45
|
}
|
|
46
|
+
function authStatus(local) {
|
|
47
|
+
if (!local.exists)
|
|
48
|
+
return 'missing';
|
|
49
|
+
return local.ok ? 'ready' : 'invalid';
|
|
50
|
+
}
|
|
51
|
+
/** Publish only safe readiness metadata; useful immediately before a repo push. */
|
|
52
|
+
export function publishReservedAuthVerdict(options = {}) {
|
|
53
|
+
const local = (options.inspectLocal ?? inspectReservedAuthBundle)();
|
|
54
|
+
const status = authStatus(local);
|
|
55
|
+
const device = options.localName ?? machineId();
|
|
56
|
+
try {
|
|
57
|
+
const write = updateFleetSharedDeviceState(device, { auth: { status } }, options.userAgentsDir ?? getUserAgentsDir());
|
|
58
|
+
return { device, status, changed: write.changed, error: null };
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
return { device, status, changed: false, error: err.message };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
67
64
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* the wrong backend (doctor flags that separately).
|
|
65
|
+
* Publish local readiness, elect one ready source, then asynchronously provision
|
|
66
|
+
* only peers whose shared verdict says the bundle is missing.
|
|
71
67
|
*/
|
|
72
|
-
export function syncReservedAuthBundle(deps = {}) {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
export async function syncReservedAuthBundle(deps = {}) {
|
|
69
|
+
const result = {
|
|
70
|
+
publisher: null,
|
|
71
|
+
stateChanged: false,
|
|
72
|
+
pushed: [],
|
|
73
|
+
skipped: [],
|
|
74
|
+
errors: [],
|
|
75
|
+
};
|
|
76
|
+
const published = publishReservedAuthVerdict(deps);
|
|
77
|
+
const localStatus = published.status;
|
|
78
|
+
const localName = published.device;
|
|
79
|
+
const localNorm = normalizeHost(localName);
|
|
80
|
+
const root = deps.userAgentsDir ?? getUserAgentsDir();
|
|
81
|
+
const devices = (deps.listDevices ?? defaultDevices)().filter((device) => normalizeHost(device.name) !== localNorm);
|
|
82
|
+
result.stateChanged = published.changed;
|
|
83
|
+
if (published.error)
|
|
84
|
+
result.errors.push({ device: localName, message: `could not publish auth verdict: ${published.error}` });
|
|
85
|
+
const read = readFleetSharedDeviceStates(root);
|
|
86
|
+
result.errors.push(...read.errors);
|
|
87
|
+
const stateByDevice = new Map(read.states.map((state) => [normalizeHost(state.device), state]));
|
|
88
|
+
const registered = new Map(devices.map((device) => [normalizeHost(device.name), device]));
|
|
89
|
+
const readyPublishers = read.states
|
|
90
|
+
.filter((state) => {
|
|
91
|
+
if (state.auth?.status !== 'ready')
|
|
92
|
+
return false;
|
|
93
|
+
if (normalizeHost(state.device) === localNorm)
|
|
94
|
+
return localStatus === 'ready';
|
|
95
|
+
const device = registered.get(normalizeHost(state.device));
|
|
96
|
+
// Removed and known-offline device files are stale observations, not live
|
|
97
|
+
// executors. Letting one win election would suppress provisioning forever.
|
|
98
|
+
return !!device && isDialableDevice(device);
|
|
99
|
+
})
|
|
100
|
+
.map((state) => state.device);
|
|
101
|
+
if (localStatus === 'ready' && !readyPublishers.some((name) => normalizeHost(name) === localNorm)) {
|
|
102
|
+
readyPublishers.push(localName);
|
|
103
|
+
}
|
|
104
|
+
readyPublishers.sort((a, b) => normalizeHost(a).localeCompare(normalizeHost(b)));
|
|
105
|
+
result.publisher = readyPublishers[0] ?? null;
|
|
106
|
+
const localIsPublisher = result.publisher !== null && normalizeHost(result.publisher) === localNorm;
|
|
78
107
|
const pinned = deps.isPinned ?? ((name) => isHostPinned(name, managedKnownHostsPath()));
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (!isPinnedDev) {
|
|
88
|
-
return { name: d.name, reachable: true, pinned: false, remoteHasAuth: false };
|
|
89
|
-
}
|
|
90
|
-
const p = probe(d);
|
|
91
|
-
return {
|
|
92
|
-
name: d.name,
|
|
93
|
-
reachable: p.reachable,
|
|
94
|
-
pinned: true,
|
|
95
|
-
remoteHasAuth: p.remoteHasAuth,
|
|
96
|
-
};
|
|
97
|
-
}));
|
|
98
|
-
const pushed = [];
|
|
99
|
-
const skipped = [];
|
|
100
|
-
const errors = [];
|
|
101
|
-
const push = deps.push ?? ((bundle, host) => pushBundleToHost(bundle, host, {
|
|
108
|
+
const plan = planAuthBundlePush(localStatus === 'ready', localIsPublisher, devices.map((device) => ({
|
|
109
|
+
name: device.name,
|
|
110
|
+
reachable: isDialableDevice(device),
|
|
111
|
+
pinned: isDevicePinned(device, pinned),
|
|
112
|
+
remoteAuth: stateByDevice.get(normalizeHost(device.name))?.auth?.status ?? 'unknown',
|
|
113
|
+
})));
|
|
114
|
+
const byName = new Map(devices.map((device) => [device.name, device]));
|
|
115
|
+
const push = deps.push ?? ((bundle, host) => pushBundleToHostAsync(bundle, host, {
|
|
102
116
|
remoteBackend: 'file',
|
|
103
117
|
operation: 'auth-sync',
|
|
118
|
+
agentOnly: true,
|
|
119
|
+
timeoutMs: AUTH_SYNC_PUSH_DEADLINE_MS,
|
|
104
120
|
}));
|
|
105
121
|
const targetOf = deps.sshTarget ?? sshTargetFor;
|
|
106
|
-
|
|
107
|
-
if (item.action === 'skip')
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (!profile) {
|
|
113
|
-
skipped.push({ device: item.device, reason: 'not in registry' });
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
122
|
+
const outcomes = await Promise.all(plan.map(async (item) => {
|
|
123
|
+
if (item.action === 'skip')
|
|
124
|
+
return { kind: 'skip', device: item.device, message: item.reason };
|
|
125
|
+
const profile = byName.get(item.device);
|
|
126
|
+
if (!profile)
|
|
127
|
+
return { kind: 'skip', device: item.device, message: 'not in registry' };
|
|
116
128
|
try {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
else
|
|
122
|
-
errors.push({ device: item.device, message: out.message });
|
|
129
|
+
const out = await push(AUTH_BUNDLE_NAME, targetOf(profile));
|
|
130
|
+
return out.ok
|
|
131
|
+
? { kind: 'pushed', device: item.device, message: out.message }
|
|
132
|
+
: { kind: 'error', device: item.device, message: out.message };
|
|
123
133
|
}
|
|
124
134
|
catch (err) {
|
|
125
|
-
|
|
135
|
+
return { kind: 'error', device: item.device, message: err.message };
|
|
126
136
|
}
|
|
137
|
+
}));
|
|
138
|
+
for (const outcome of outcomes) {
|
|
139
|
+
if (outcome.kind === 'pushed')
|
|
140
|
+
result.pushed.push(outcome.device);
|
|
141
|
+
else if (outcome.kind === 'skip')
|
|
142
|
+
result.skipped.push({ device: outcome.device, reason: outcome.message });
|
|
143
|
+
else
|
|
144
|
+
result.errors.push({ device: outcome.device, message: outcome.message });
|
|
127
145
|
}
|
|
128
|
-
return
|
|
146
|
+
return result;
|
|
129
147
|
}
|
|
@@ -55,7 +55,7 @@ export declare function attributedSetLostPids(prev: Set<number>, next: Set<numbe
|
|
|
55
55
|
export declare function filterCachedUnattributed(sessions: ActiveSession[], attributed: Set<number>, alive: (pid: number, startedAtMs?: number) => boolean): ActiveSession[];
|
|
56
56
|
type ActiveContext = 'terminal' | 'teams' | 'cloud' | 'headless';
|
|
57
57
|
/** The SessionMeta fields the live-row backfill reads — the enrichment a running process cannot report. */
|
|
58
|
-
export type BackfillMeta = Pick<SessionMeta, 'version' | 'account' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName' | 'harness' | 'tokenCount' | 'durationMs' | 'subAgentCount'>;
|
|
58
|
+
export type BackfillMeta = Pick<SessionMeta, 'version' | 'account' | 'timestamp' | 'label' | 'firstUserMessage' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName' | 'harness' | 'tokenCount' | 'durationMs' | 'subAgentCount'>;
|
|
59
59
|
export declare function backfillActiveRowsFromMeta(sessions: ActiveSession[], metaById: Map<string, BackfillMeta>): void;
|
|
60
60
|
export declare function backfillActiveRowsFromIndex(sessions: ActiveSession[]): void;
|
|
61
61
|
export declare function isRunningLiveSession(s: ActiveSession): boolean;
|
|
@@ -138,6 +138,8 @@ export interface ActiveSession {
|
|
|
138
138
|
name?: string;
|
|
139
139
|
/** First meaningful line of the initial prompt (extracted topic). */
|
|
140
140
|
topic?: string;
|
|
141
|
+
/** Full, cleaned first genuine user turn, backfilled from the session index. */
|
|
142
|
+
firstUserMessage?: string;
|
|
141
143
|
/**
|
|
142
144
|
* The row's shown title — WHAT the session is, best-source-wins (RUSH-3011).
|
|
143
145
|
* The ladder ({@link deriveSessionRecap}): a `/rename` or harness `label` →
|
|
@@ -408,6 +410,16 @@ export interface ActiveSession {
|
|
|
408
410
|
* once a session id resolves (the id is the identity then).
|
|
409
411
|
*/
|
|
410
412
|
paneId?: string;
|
|
413
|
+
/**
|
|
414
|
+
* The `ag-<agent>-<shortid>` tmux session name this row was discovered under,
|
|
415
|
+
* when it came from the tmux source. Kept even when the full session id could
|
|
416
|
+
* NOT be resolved (the row's `sessionId` is then absent), because the name's
|
|
417
|
+
* `<shortid>` suffix is the only stable selector such a row exposes — it is
|
|
418
|
+
* what `agents sessions --active` prints as the tmux target and what
|
|
419
|
+
* `agents sessions inject <shortid>` matches on to nudge an id-less remote
|
|
420
|
+
* session (PHNX-3688). Absent for non-tmux rows.
|
|
421
|
+
*/
|
|
422
|
+
tmuxName?: string;
|
|
411
423
|
}
|
|
412
424
|
export declare function activeStatusFromCloudStatus(status: CloudTaskStatus): ActiveStatus;
|
|
413
425
|
interface ActiveQueryOptions {
|
|
@@ -149,6 +149,8 @@ export function backfillActiveRowsFromMeta(sessions, metaById) {
|
|
|
149
149
|
s.account = m.account;
|
|
150
150
|
if (!s.label && m.label)
|
|
151
151
|
s.label = m.label;
|
|
152
|
+
if (!s.firstUserMessage && m.firstUserMessage)
|
|
153
|
+
s.firstUserMessage = m.firstUserMessage;
|
|
152
154
|
if (!s.ticket && m.ticketId)
|
|
153
155
|
s.ticket = { id: m.ticketId, url: linearIssueUrl(m.ticketId) };
|
|
154
156
|
if (!s.pr && m.prUrl)
|
|
@@ -1775,6 +1777,9 @@ export async function listTmuxAgentSessions() {
|
|
|
1775
1777
|
// An id-less pane keys its dedupe on the unique pane, so two anonymous
|
|
1776
1778
|
// co-located panes stay two rows instead of folding into one.
|
|
1777
1779
|
paneId: id.sessionId ?? sessionIdFromFile(sessionFile) ? undefined : pane,
|
|
1780
|
+
// The tmux session name is the one selector an id-less row still exposes —
|
|
1781
|
+
// its `<shortid>` suffix is what `sessions inject` matches on (PHNX-3688).
|
|
1782
|
+
tmuxName: sessName,
|
|
1778
1783
|
}, state, sessionFile, pidAlive));
|
|
1779
1784
|
}
|
|
1780
1785
|
return out;
|
|
@@ -7,6 +7,18 @@ export interface SessionActorRecord {
|
|
|
7
7
|
initiatedBy?: 'human' | 'agent';
|
|
8
8
|
/** Effective permissions mode used by the launcher. */
|
|
9
9
|
mode?: SessionRunMode;
|
|
10
|
+
/**
|
|
11
|
+
* The agents-cli version-home id this session launched under (e.g. `2.1.207`,
|
|
12
|
+
* codex `0.146.0`) — the same namespace `listInstalledVersions` /
|
|
13
|
+
* `collectRunCandidates` use, so a native resume can pin the exact origin
|
|
14
|
+
* version. Recorded at launch by the SessionStart hook (from `AGENTS_RUN_VERSION`),
|
|
15
|
+
* because a harness coins its real session id only AFTER spawn — the same reason
|
|
16
|
+
* `mode` rides the hook rather than a spawn-time `writeSessionActorRecord`. Joined
|
|
17
|
+
* onto the session index at scan time so a session whose transcript carries no
|
|
18
|
+
* embedded/derivable version (codex's `.codex-homes/<version>/` layout) no longer
|
|
19
|
+
* degrades native resume to `/continue` for lack of a recorded origin (PHNX-3626).
|
|
20
|
+
*/
|
|
21
|
+
version?: string;
|
|
10
22
|
/**
|
|
11
23
|
* Custom harness / profile name when launched via `agents run <profile>`
|
|
12
24
|
* (e.g. `deepseek`). Joined onto the session index at scan time so a
|
|
@@ -40,6 +40,7 @@ function isSafeAlias(alias) {
|
|
|
40
40
|
function hasRecordData(record) {
|
|
41
41
|
return typeof record.actor === 'string'
|
|
42
42
|
|| typeof record.mode === 'string'
|
|
43
|
+
|| typeof record.version === 'string'
|
|
43
44
|
|| typeof record.harness === 'string'
|
|
44
45
|
|| (Array.isArray(record.aliases) && record.aliases.some(alias => typeof alias === 'string'));
|
|
45
46
|
}
|
|
@@ -84,6 +85,7 @@ export function writeSessionAliasRecord(sessionId, alias) {
|
|
|
84
85
|
actor: previous?.actor,
|
|
85
86
|
initiatedBy: previous?.initiatedBy,
|
|
86
87
|
mode: previous?.mode,
|
|
88
|
+
version: previous?.version,
|
|
87
89
|
harness: previous?.harness,
|
|
88
90
|
aliases: normalizedAliases([...(previous?.aliases ?? []), alias]),
|
|
89
91
|
startedAtMs: previous?.startedAtMs ?? Date.now(),
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { type ToolScanResumePoint } from './tool-store.js';
|
|
|
13
13
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
14
14
|
* assert against the constant instead of hardcoding a number that every bump
|
|
15
15
|
* then has to chase (docs/sessions.md calls the constant the source of truth). */
|
|
16
|
-
export declare const SCHEMA_VERSION =
|
|
16
|
+
export declare const SCHEMA_VERSION = 45;
|
|
17
17
|
/**
|
|
18
18
|
* Bump to force the content extractor (assistant-answer text, alongside the
|
|
19
19
|
* user-prompt text every harness already accumulates) to re-derive on every
|
|
@@ -24,8 +24,13 @@ export declare const SCHEMA_VERSION = 44;
|
|
|
24
24
|
* here backfills every existing session's assistant text on its next scan
|
|
25
25
|
* without a `DELETE FROM scan_ledger` (which would also throw away the
|
|
26
26
|
* resumable parser_state/content_text for Claude/Codex).
|
|
27
|
+
*
|
|
28
|
+
* v3 (PHNX-3621) invalidates OpenCode rows that merged v2 stamped before its
|
|
29
|
+
* genuine first-user-turn extractor was added.
|
|
30
|
+
* v4 (PHNX-3621 leftover) invalidates Grok rows stamped before the bounded
|
|
31
|
+
* chat_history.jsonl prefix read filled firstUserMessage.
|
|
27
32
|
*/
|
|
28
|
-
export declare const CONTENT_INDEX_VERSION =
|
|
33
|
+
export declare const CONTENT_INDEX_VERSION = 4;
|
|
29
34
|
/**
|
|
30
35
|
* Bumping this invalidates every cached facet row without touching the schema
|
|
31
36
|
* version, so a change to the extraction logic (a new metric, a corrected bucket)
|
|
@@ -620,6 +625,15 @@ export declare function findSessionMachinesByIds(ids: string[]): Map<string, str
|
|
|
620
625
|
export declare function getSessionById(id: string): SessionMeta | null;
|
|
621
626
|
/** Exact ids win over prefixes; the normal existence check preserves archived content but excludes phantoms. */
|
|
622
627
|
export declare function findSessionsById(idQuery: string, scope?: Pick<QueryOptions, 'agent' | 'version' | 'cwd' | 'project'>): SessionMeta[];
|
|
628
|
+
/**
|
|
629
|
+
* Upgrade a session-id crumb to the full indexed id. The 8-char hex `short_id`
|
|
630
|
+
* a "Sent from …" footer shows would 404 as a `…/console/sessions/<id>` URL, so
|
|
631
|
+
* an owner ping resolves it through the index first (PHNX-3698). A value that is
|
|
632
|
+
* already a full id (or any non-crumb shape, e.g. a native `ses_…` id), or a
|
|
633
|
+
* crumb the index cannot resolve, is returned unchanged — the caller keeps
|
|
634
|
+
* exactly what it had rather than getting a fabricated id.
|
|
635
|
+
*/
|
|
636
|
+
export declare function resolveFullSessionId(idOrCrumb: string | undefined): string | undefined;
|
|
623
637
|
/** Batch-resolve pane short ids; on collision the most recently active session wins. */
|
|
624
638
|
export declare function findSessionsByShortIds(shortIds: string[]): Map<string, SessionMeta>;
|
|
625
639
|
/** A single full-text search result with ranking score. */
|
package/dist/lib/session/db.js
CHANGED
|
@@ -22,12 +22,13 @@ import { extractBackgroundShells, extractSkills, extractSlashCommands, harnessTr
|
|
|
22
22
|
import { resolveResource } from '../resources.js';
|
|
23
23
|
import { discoverPlugins } from '../plugins/plugins.js';
|
|
24
24
|
import { machineId } from '../machine-id.js';
|
|
25
|
+
import { firstUserMessageFromEvents } from './prompt.js';
|
|
25
26
|
const SESSIONS_DIR = getSessionsDir();
|
|
26
27
|
const DB_PATH = getSessionsDbPath();
|
|
27
28
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
28
29
|
* assert against the constant instead of hardcoding a number that every bump
|
|
29
30
|
* then has to chase (docs/sessions.md calls the constant the source of truth). */
|
|
30
|
-
export const SCHEMA_VERSION =
|
|
31
|
+
export const SCHEMA_VERSION = 45;
|
|
31
32
|
/**
|
|
32
33
|
* Bump to force the content extractor (assistant-answer text, alongside the
|
|
33
34
|
* user-prompt text every harness already accumulates) to re-derive on every
|
|
@@ -38,8 +39,13 @@ export const SCHEMA_VERSION = 44;
|
|
|
38
39
|
* here backfills every existing session's assistant text on its next scan
|
|
39
40
|
* without a `DELETE FROM scan_ledger` (which would also throw away the
|
|
40
41
|
* resumable parser_state/content_text for Claude/Codex).
|
|
42
|
+
*
|
|
43
|
+
* v3 (PHNX-3621) invalidates OpenCode rows that merged v2 stamped before its
|
|
44
|
+
* genuine first-user-turn extractor was added.
|
|
45
|
+
* v4 (PHNX-3621 leftover) invalidates Grok rows stamped before the bounded
|
|
46
|
+
* chat_history.jsonl prefix read filled firstUserMessage.
|
|
41
47
|
*/
|
|
42
|
-
export const CONTENT_INDEX_VERSION =
|
|
48
|
+
export const CONTENT_INDEX_VERSION = 4;
|
|
43
49
|
/**
|
|
44
50
|
* Bump to force `agents sessions backfill resources` to re-derive every
|
|
45
51
|
* session's skill/slash-command tallies on its next run (resource_scan_ledger
|
|
@@ -96,6 +102,7 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
96
102
|
cwd TEXT,
|
|
97
103
|
git_branch TEXT,
|
|
98
104
|
topic TEXT,
|
|
105
|
+
first_user_message TEXT,
|
|
99
106
|
label TEXT,
|
|
100
107
|
message_count INTEGER,
|
|
101
108
|
token_count INTEGER,
|
|
@@ -1231,6 +1238,15 @@ function migrateSchema(db, fromVersion) {
|
|
|
1231
1238
|
}
|
|
1232
1239
|
}
|
|
1233
1240
|
}
|
|
1241
|
+
if (fromVersion < 45) {
|
|
1242
|
+
// v44 -> v45: retain the full first genuine user turn separately from the
|
|
1243
|
+
// one-line topic. The CONTENT_INDEX_VERSION bump re-parses every transcript
|
|
1244
|
+
// through its normal incremental scan path so existing rows backfill without
|
|
1245
|
+
// a second consumer-side transcript reader.
|
|
1246
|
+
const cols = new Set(db.prepare(`PRAGMA table_info(sessions)`).all().map((c) => c.name));
|
|
1247
|
+
if (!cols.has('first_user_message'))
|
|
1248
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN first_user_message TEXT`);
|
|
1249
|
+
}
|
|
1234
1250
|
}
|
|
1235
1251
|
/**
|
|
1236
1252
|
* Stamp `account_key` / `account_org` / `account` on every Claude row from its
|
|
@@ -1567,9 +1583,11 @@ export function getDBPath() {
|
|
|
1567
1583
|
export function getScanStampByPath(filePath) {
|
|
1568
1584
|
const db = getDB();
|
|
1569
1585
|
const row = db
|
|
1570
|
-
.prepare(`SELECT file_mtime_ms, file_size, scanned_at FROM scan_ledger WHERE file_path = ? LIMIT 1`)
|
|
1586
|
+
.prepare(`SELECT file_mtime_ms, file_size, scanned_at, extractor_version FROM scan_ledger WHERE file_path = ? LIMIT 1`)
|
|
1571
1587
|
.get(canonicalLedgerKey(filePath));
|
|
1572
|
-
return row
|
|
1588
|
+
return row
|
|
1589
|
+
? { fileMtimeMs: row.file_mtime_ms, fileSize: row.file_size, scannedAt: row.scanned_at, extractorVersion: row.extractor_version }
|
|
1590
|
+
: null;
|
|
1573
1591
|
}
|
|
1574
1592
|
/**
|
|
1575
1593
|
* Bulk-load the stamp ledger for a set of file paths in a single SQL query.
|
|
@@ -1771,7 +1789,7 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
1771
1789
|
INSERT INTO sessions (
|
|
1772
1790
|
id, short_id, agent, harness, origin, routine_name, routine_run_id,
|
|
1773
1791
|
version, account, account_key, account_org, mode, timestamp, last_activity,
|
|
1774
|
-
project, cwd, git_branch, topic, label, message_count, token_count,
|
|
1792
|
+
project, cwd, git_branch, topic, first_user_message, label, message_count, token_count,
|
|
1775
1793
|
output_tokens, input_tokens, cache_read_tokens, cache_write_tokens,
|
|
1776
1794
|
cost_usd, cost_usd_nocache, duration_ms, model, tool_call_count,
|
|
1777
1795
|
file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
|
|
@@ -1782,7 +1800,7 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
1782
1800
|
) VALUES (
|
|
1783
1801
|
@id, @short_id, @agent, @harness, @origin, @routine_name, @routine_run_id,
|
|
1784
1802
|
@version, @account, @account_key, @account_org, @mode, @timestamp, @last_activity,
|
|
1785
|
-
@project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
|
|
1803
|
+
@project, @cwd, @git_branch, @topic, @first_user_message, @label, @message_count, @token_count,
|
|
1786
1804
|
@output_tokens, @input_tokens, @cache_read_tokens, @cache_write_tokens,
|
|
1787
1805
|
@cost_usd, @cost_usd_nocache, @duration_ms, @model, @tool_call_count,
|
|
1788
1806
|
@file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
|
|
@@ -1802,7 +1820,13 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
1802
1820
|
origin = excluded.origin,
|
|
1803
1821
|
routine_name = excluded.routine_name,
|
|
1804
1822
|
routine_run_id = excluded.routine_run_id,
|
|
1805
|
-
version
|
|
1823
|
+
-- Origin version is write-once launch metadata, like mode/harness/actor:
|
|
1824
|
+
-- backfill a NULL-first row once the launch sidecar lands, and keep the
|
|
1825
|
+
-- recorded version across a rescan that cannot re-derive it (a codex
|
|
1826
|
+
-- transcript outside a versions/agent/ path, or the durable sidecar aged
|
|
1827
|
+
-- out) — the same COALESCE the launch-metadata columns use, so native resume
|
|
1828
|
+
-- stops falling back for a missing recorded origin (PHNX-3626).
|
|
1829
|
+
version = COALESCE(excluded.version, sessions.version),
|
|
1806
1830
|
account = excluded.account,
|
|
1807
1831
|
account_key = excluded.account_key,
|
|
1808
1832
|
account_org = excluded.account_org,
|
|
@@ -1813,6 +1837,7 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
1813
1837
|
cwd = excluded.cwd,
|
|
1814
1838
|
git_branch = excluded.git_branch,
|
|
1815
1839
|
topic = excluded.topic,
|
|
1840
|
+
first_user_message = COALESCE(excluded.first_user_message, sessions.first_user_message),
|
|
1816
1841
|
-- Never let an empty/placeholder incoming label clobber a good stored one.
|
|
1817
1842
|
-- A real incoming label (non-empty after trim) still wins; a blank one keeps
|
|
1818
1843
|
-- the label seeded by --name (seedLabelsFromNames) or refined by an agent
|
|
@@ -2006,18 +2031,23 @@ function fanOutCounts(events, agent) {
|
|
|
2006
2031
|
: undefined,
|
|
2007
2032
|
};
|
|
2008
2033
|
}
|
|
2034
|
+
/** Fold transcript-derived metadata that every parser can supply uniformly. */
|
|
2035
|
+
function enrichMetaFromEvents(meta, events) {
|
|
2036
|
+
return {
|
|
2037
|
+
...meta,
|
|
2038
|
+
firstUserMessage: meta.firstUserMessage ?? firstUserMessageFromEvents(events),
|
|
2039
|
+
todos: extractTodoProgressFromEvents(events),
|
|
2040
|
+
recentDirectoriesTouched: extractRecentDirectoriesTouched(events, meta.cwd),
|
|
2041
|
+
...fanOutCounts(events, meta.agent),
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2009
2044
|
function enrichCachedSessionMeta(meta) {
|
|
2010
2045
|
if (!meta.filePath)
|
|
2011
2046
|
return meta;
|
|
2012
2047
|
try {
|
|
2013
2048
|
const events = parseSession(meta.filePath, meta.agent);
|
|
2014
2049
|
writeResourceUsage(meta.id, events, meta.cwd);
|
|
2015
|
-
return
|
|
2016
|
-
...meta,
|
|
2017
|
-
todos: extractTodoProgressFromEvents(events),
|
|
2018
|
-
recentDirectoriesTouched: extractRecentDirectoriesTouched(events, meta.cwd),
|
|
2019
|
-
...fanOutCounts(events, meta.agent),
|
|
2020
|
-
};
|
|
2050
|
+
return enrichMetaFromEvents(meta, events);
|
|
2021
2051
|
}
|
|
2022
2052
|
catch {
|
|
2023
2053
|
// Synthetic/cloud rows can intentionally name a transcript that is not local.
|
|
@@ -2081,7 +2111,11 @@ export function upsertSession(meta, content, scan, assistantContent = '') {
|
|
|
2081
2111
|
origin: meta.origin ?? 'cli',
|
|
2082
2112
|
routine_name: meta.routineName ?? null,
|
|
2083
2113
|
routine_run_id: meta.routineRunId ?? null,
|
|
2084
|
-
|
|
2114
|
+
// Backfill the origin version from the launch-time sidecar when the transcript
|
|
2115
|
+
// scan couldn't derive one (codex's `.codex-homes/<version>/` home) — the same
|
|
2116
|
+
// write-once join as mode/harness, so native resume stops falling back to
|
|
2117
|
+
// `/continue` for a missing recorded version (PHNX-3626).
|
|
2118
|
+
version: meta.version ?? actorRec?.version ?? null,
|
|
2085
2119
|
account: meta.account ?? null,
|
|
2086
2120
|
account_key: meta.accountKey ?? null,
|
|
2087
2121
|
account_org: meta.accountOrg ?? null,
|
|
@@ -2092,6 +2126,7 @@ export function upsertSession(meta, content, scan, assistantContent = '') {
|
|
|
2092
2126
|
cwd: meta.cwd ?? null,
|
|
2093
2127
|
git_branch: meta.gitBranch ?? null,
|
|
2094
2128
|
topic: meta.topic ?? null,
|
|
2129
|
+
first_user_message: meta.firstUserMessage ?? null,
|
|
2095
2130
|
label: meta.label ?? null,
|
|
2096
2131
|
message_count: meta.messageCount ?? null,
|
|
2097
2132
|
token_count: meta.tokenCount ?? null,
|
|
@@ -2186,7 +2221,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
2186
2221
|
// and means "not computed yet", and the next scan that carries events fills
|
|
2187
2222
|
// it. Never collapse that to 0: see fanOutCounts.
|
|
2188
2223
|
return entry.events
|
|
2189
|
-
? { ...entry, meta:
|
|
2224
|
+
? { ...entry, meta: enrichMetaFromEvents(entry.meta, entry.events) }
|
|
2190
2225
|
: entry;
|
|
2191
2226
|
}
|
|
2192
2227
|
// Harnesses whose scanners produce no events AND whose parseSession reads a
|
|
@@ -2233,12 +2268,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
2233
2268
|
const scanned = scanEventToolCalls(events, prior ?? undefined);
|
|
2234
2269
|
return {
|
|
2235
2270
|
...entry,
|
|
2236
|
-
meta:
|
|
2237
|
-
...entry.meta,
|
|
2238
|
-
todos: extractTodoProgressFromEvents(events),
|
|
2239
|
-
recentDirectoriesTouched: extractRecentDirectoriesTouched(events, entry.meta.cwd),
|
|
2240
|
-
...fanOutCounts(events, entry.meta.agent),
|
|
2241
|
-
},
|
|
2271
|
+
meta: enrichMetaFromEvents(entry.meta, events),
|
|
2242
2272
|
// The CHANGED calls only. On a resume these are the newly appended tail
|
|
2243
2273
|
// (append-safe upsert); on a full scan they are the whole history.
|
|
2244
2274
|
toolCalls: scanned.calls,
|
|
@@ -2322,7 +2352,9 @@ export function upsertSessionsBatch(entries) {
|
|
|
2322
2352
|
origin: meta.origin ?? 'cli',
|
|
2323
2353
|
routine_name: meta.routineName ?? null,
|
|
2324
2354
|
routine_run_id: meta.routineRunId ?? null,
|
|
2325
|
-
|
|
2355
|
+
// Backfill the origin version from the launch-time sidecar when the scan
|
|
2356
|
+
// couldn't derive one — see the single-row upsert above (PHNX-3626).
|
|
2357
|
+
version: meta.version ?? actorIndex.get(meta.id)?.version ?? null,
|
|
2326
2358
|
account: meta.account ?? null,
|
|
2327
2359
|
account_key: meta.accountKey ?? null,
|
|
2328
2360
|
account_org: meta.accountOrg ?? null,
|
|
@@ -2333,6 +2365,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
2333
2365
|
cwd: meta.cwd ?? null,
|
|
2334
2366
|
git_branch: meta.gitBranch ?? null,
|
|
2335
2367
|
topic: meta.topic ?? null,
|
|
2368
|
+
first_user_message: meta.firstUserMessage ?? null,
|
|
2336
2369
|
label: meta.label ?? null,
|
|
2337
2370
|
message_count: meta.messageCount ?? null,
|
|
2338
2371
|
token_count: meta.tokenCount ?? null,
|
|
@@ -2580,6 +2613,7 @@ function rowToMeta(row) {
|
|
|
2580
2613
|
accountOrg: row.account_org ?? undefined,
|
|
2581
2614
|
mode: isSessionRunMode(row.mode) ? row.mode : undefined,
|
|
2582
2615
|
topic: row.topic ?? undefined,
|
|
2616
|
+
firstUserMessage: row.first_user_message ?? undefined,
|
|
2583
2617
|
label: row.label ?? undefined,
|
|
2584
2618
|
isTeamOrigin: row.is_team_origin === 1,
|
|
2585
2619
|
prUrl: row.pr_url ?? undefined,
|
|
@@ -3679,6 +3713,23 @@ export function findSessionsById(idQuery, scope = {}) {
|
|
|
3679
3713
|
return exact;
|
|
3680
3714
|
return querySessions({ ...scope, idPrefix: q });
|
|
3681
3715
|
}
|
|
3716
|
+
/**
|
|
3717
|
+
* Upgrade a session-id crumb to the full indexed id. The 8-char hex `short_id`
|
|
3718
|
+
* a "Sent from …" footer shows would 404 as a `…/console/sessions/<id>` URL, so
|
|
3719
|
+
* an owner ping resolves it through the index first (PHNX-3698). A value that is
|
|
3720
|
+
* already a full id (or any non-crumb shape, e.g. a native `ses_…` id), or a
|
|
3721
|
+
* crumb the index cannot resolve, is returned unchanged — the caller keeps
|
|
3722
|
+
* exactly what it had rather than getting a fabricated id.
|
|
3723
|
+
*/
|
|
3724
|
+
export function resolveFullSessionId(idOrCrumb) {
|
|
3725
|
+
const id = idOrCrumb?.trim();
|
|
3726
|
+
if (!id)
|
|
3727
|
+
return undefined;
|
|
3728
|
+
if (!/^[0-9a-f]{8}$/i.test(id))
|
|
3729
|
+
return id; // already a full id (or non-hex shape) — nothing to resolve
|
|
3730
|
+
const hit = findSessionsByShortIds([id]).get(id.toLowerCase());
|
|
3731
|
+
return hit?.id ?? id;
|
|
3732
|
+
}
|
|
3682
3733
|
/** Batch-resolve pane short ids; on collision the most recently active session wins. */
|
|
3683
3734
|
export function findSessionsByShortIds(shortIds) {
|
|
3684
3735
|
const out = new Map();
|