@phnx-labs/agents-cli 1.22.75 → 1.22.77
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 +121 -0
- package/README.md +20 -8
- package/dist/bootstrap.js +7 -7
- package/dist/cli/command-registry.js +5 -0
- package/dist/commands/artifacts-setup.js +1 -1
- package/dist/commands/artifacts.js +1 -1
- package/dist/commands/auth.js +7 -1
- package/dist/commands/browser.js +104 -10
- package/dist/commands/commands.js +7 -6
- package/dist/commands/config.js +27 -4
- package/dist/commands/cost.js +6 -4
- package/dist/commands/doctor.d.ts +6 -5
- package/dist/commands/doctor.js +32 -274
- package/dist/commands/exec.d.ts +2 -0
- package/dist/commands/exec.js +9 -2
- package/dist/commands/harness.d.ts +1 -0
- package/dist/commands/harness.js +11 -3
- package/dist/commands/hooks.js +7 -6
- package/dist/commands/mcp.js +7 -6
- package/dist/commands/memory.js +7 -7
- package/dist/commands/monitors.js +3 -2
- package/dist/commands/open.d.ts +25 -12
- package/dist/commands/open.js +24 -10
- package/dist/commands/permissions.js +7 -6
- package/dist/commands/plugins.js +21 -17
- package/dist/commands/route.js +33 -16
- package/dist/commands/rules.js +7 -12
- package/dist/commands/sessions-share.js +1 -1
- package/dist/commands/setup-watchdog.js +2 -2
- package/dist/commands/setup.js +22 -1
- package/dist/commands/share.js +26 -10
- package/dist/commands/skills.js +7 -6
- package/dist/commands/subagents.js +7 -6
- package/dist/commands/sync.js +81 -10
- package/dist/commands/view.js +4 -1
- package/dist/commands/watchdog.d.ts +1 -1
- package/dist/commands/watchdog.js +10 -10
- package/dist/commands/webhook.d.ts +4 -0
- package/dist/commands/webhook.js +22 -4
- package/dist/commands/workflows.js +7 -6
- package/dist/lib/accounting/rotate.d.ts +3 -1
- package/dist/lib/accounting/rotate.js +8 -4
- package/dist/lib/auth-health.d.ts +2 -0
- package/dist/lib/auth-health.js +2 -0
- package/dist/lib/browser/chrome.d.ts +21 -0
- package/dist/lib/browser/chrome.js +60 -3
- package/dist/lib/browser/drivers/local.d.ts +21 -0
- package/dist/lib/browser/drivers/local.js +102 -9
- package/dist/lib/browser/profiles.d.ts +29 -1
- package/dist/lib/browser/profiles.js +50 -1
- package/dist/lib/browser/registry.js +10 -3
- package/dist/lib/browser/types.d.ts +18 -0
- package/dist/lib/config-keys.d.ts +7 -2
- package/dist/lib/config-keys.js +17 -2
- package/dist/lib/daemon/daemon.js +8 -0
- package/dist/lib/daemon/session-summarizer-service.d.ts +24 -0
- package/dist/lib/daemon/session-summarizer-service.js +39 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/daemon-ticks.d.ts +2 -2
- package/dist/lib/daemon-ticks.js +2 -1
- package/dist/lib/deeplink/register.js +10 -9
- package/dist/lib/deeplink/url.d.ts +4 -4
- package/dist/lib/deeplink/url.js +4 -4
- package/dist/lib/device-config.js +25 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -4
- package/dist/lib/devices/doctor-findings.js +14 -8
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/stats-cache.d.ts +4 -0
- package/dist/lib/devices/stats-cache.js +19 -0
- package/dist/lib/drift-sync.d.ts +3 -1
- package/dist/lib/drift-sync.js +16 -5
- package/dist/lib/exec.d.ts +2 -0
- package/dist/lib/exec.js +16 -1
- package/dist/lib/fleet-shared-state.d.ts +8 -0
- package/dist/lib/git.js +6 -0
- package/dist/lib/heal.d.ts +4 -3
- package/dist/lib/heal.js +5 -4
- package/dist/lib/hosts/ready.d.ts +1 -1
- package/dist/lib/hosts/ready.js +16 -4
- package/dist/lib/hosts/reconnect.js +4 -2
- package/dist/lib/identity/client.d.ts +6 -0
- package/dist/lib/identity/index.d.ts +16 -0
- package/dist/lib/identity/index.js +25 -1
- package/dist/lib/installations/migrate.d.ts +10 -0
- package/dist/lib/installations/migrate.js +18 -0
- package/dist/lib/profiles.d.ts +2 -0
- package/dist/lib/profiles.js +28 -9
- package/dist/lib/project-resources.d.ts +23 -0
- package/dist/lib/project-resources.js +94 -0
- package/dist/lib/reconcile-and-repair.d.ts +109 -0
- package/dist/lib/reconcile-and-repair.js +267 -0
- package/dist/lib/routers.d.ts +12 -1
- package/dist/lib/routers.js +30 -1
- package/dist/lib/scheduling/routines.js +8 -2
- package/dist/lib/session/active.d.ts +13 -0
- package/dist/lib/session/db.d.ts +47 -7
- package/dist/lib/session/db.js +114 -12
- package/dist/lib/session/mirror.js +58 -0
- package/dist/lib/session/remote/watch.js +22 -2
- package/dist/lib/session/session-cache.d.ts +19 -0
- package/dist/lib/session/session-cache.js +46 -0
- package/dist/lib/session/types.d.ts +34 -0
- package/dist/lib/share/backend.d.ts +6 -4
- package/dist/lib/share/backend.js +10 -8
- package/dist/lib/share/config.d.ts +4 -3
- package/dist/lib/share/config.js +10 -1
- package/dist/lib/share/delete.d.ts +1 -1
- package/dist/lib/share/delete.js +1 -1
- package/dist/lib/share/html.d.ts +1 -1
- package/dist/lib/share/html.js +1 -1
- package/dist/lib/share/provision.d.ts +1 -1
- package/dist/lib/share/provision.js +2 -2
- package/dist/lib/share/publish.d.ts +23 -7
- package/dist/lib/share/publish.js +58 -12
- package/dist/lib/share/worker-template.js +221 -60
- package/dist/lib/startup/command-registry.js +2 -2
- package/dist/lib/state.d.ts +58 -1
- package/dist/lib/state.js +138 -16
- package/dist/lib/summarizer/config.d.ts +46 -0
- package/dist/lib/summarizer/config.js +83 -0
- package/dist/lib/summarizer/pass.d.ts +45 -0
- package/dist/lib/summarizer/pass.js +112 -0
- package/dist/lib/summarizer/summarize.d.ts +68 -0
- package/dist/lib/summarizer/summarize.js +120 -0
- package/dist/lib/teams/agents.d.ts +4 -3
- package/dist/lib/teams/agents.js +12 -4
- package/dist/lib/teams/scheduler.d.ts +4 -2
- package/dist/lib/teams/scheduler.js +6 -6
- package/dist/lib/tmux/session.d.ts +2 -0
- package/dist/lib/tmux/session.js +7 -1
- package/dist/lib/types.d.ts +20 -0
- package/dist/lib/verbs.d.ts +23 -0
- package/dist/lib/verbs.js +24 -0
- package/dist/lib/view-types.d.ts +4 -0
- package/dist/lib/watchdog/rotate.d.ts +1 -1
- package/dist/lib/watchdog/rotate.js +1 -1
- package/package.json +1 -1
package/dist/lib/state.d.ts
CHANGED
|
@@ -319,6 +319,21 @@ export declare function getRuntimeStateDir(): string;
|
|
|
319
319
|
export declare function getCompanionDir(): string;
|
|
320
320
|
/** Path to browser runtime data — chrome-data, pids (~/.agents/.cache/browser/). */
|
|
321
321
|
export declare function getBrowserRuntimeDir(): string;
|
|
322
|
+
/**
|
|
323
|
+
* Path to DURABLE browser-profile data (~/.agents/.history/browser-profiles/).
|
|
324
|
+
*
|
|
325
|
+
* This is the persistent home for an attach-only profile's `--user-data-dir` —
|
|
326
|
+
* where a one-time browser sign-in lives. It sits under `.history` (durable),
|
|
327
|
+
* NOT `.cache` (regenerable), for two reasons the ticket (PHNX-3967) named:
|
|
328
|
+
* - `agents browser profiles remove` sweeps `~/.agents/.cache/browser/<name>*`;
|
|
329
|
+
* a durable dir here survives that so logins are not wiped by a routine cleanup.
|
|
330
|
+
* - A cache wipe or the daemon reaper never touches it, so a signed-in Comet
|
|
331
|
+
* survives quit+relaunch.
|
|
332
|
+
* The user's canonical Comet is launched with this as `--user-data-dir`, and the
|
|
333
|
+
* ownership guard in the local driver compares the running instance's
|
|
334
|
+
* `--user-data-dir` against it to reject a foreign port-squatter.
|
|
335
|
+
*/
|
|
336
|
+
export declare function getBrowserDurableDir(): string;
|
|
322
337
|
/** Path to helper subprocess scratch (~/.agents/.cache/helpers/). */
|
|
323
338
|
export declare function getHelpersDir(): string;
|
|
324
339
|
/**
|
|
@@ -394,6 +409,33 @@ export declare function getDeviceMetaPath(): string;
|
|
|
394
409
|
*/
|
|
395
410
|
export declare function getVersionResourcesPath(): string;
|
|
396
411
|
export declare function withMetaLock<T>(fn: () => T): T;
|
|
412
|
+
/**
|
|
413
|
+
* Commit the central `agents.yaml` in the user repo, synchronously, so a CLI
|
|
414
|
+
* config mutation never leaves the working tree dirty on that one shared-line
|
|
415
|
+
* file at rest.
|
|
416
|
+
*
|
|
417
|
+
* Why this exists: CLI config commands rewrite the fleet-shared central
|
|
418
|
+
* agents.yaml as a plain file write. Left uncommitted, the tree is dirty on
|
|
419
|
+
* agents.yaml between the write and the daemon's next 15-min publish tick — and
|
|
420
|
+
* a peer's incoming publish commit (which also touches agents.yaml) then trips
|
|
421
|
+
* `dirtyTreeRefusal` ("incoming changes touch uncommitted paths: agents.yaml"),
|
|
422
|
+
* wedging `agents repo pull` fleet-wide (PHNX-3968). Committing the central edit
|
|
423
|
+
* in the same command that made it closes that window: agents.yaml is clean at
|
|
424
|
+
* rest, so nothing incoming can collide with it. Called AFTER the meta lock
|
|
425
|
+
* releases (see {@link commitCentralConfigAfterWrite}) so the git subprocesses
|
|
426
|
+
* never run inside the short, non-heartbeated lockfile window; the user repo
|
|
427
|
+
* already pushes.
|
|
428
|
+
*
|
|
429
|
+
* Scoped tightly: only called when the central bytes actually changed, and never
|
|
430
|
+
* from the daemon (see {@link isDaemonProcess}). A commit failure fails open —
|
|
431
|
+
* a concurrent daemon holding `index.lock`, a mid-rebase repo — leaving
|
|
432
|
+
* agents.yaml dirty; the next successful central write commits it, and until
|
|
433
|
+
* then a pull that would collide refuses rather than losing data. A config
|
|
434
|
+
* command must never fail because git hiccuped.
|
|
435
|
+
*
|
|
436
|
+
* Returns whether a commit was created. Exported for the real-repo tests.
|
|
437
|
+
*/
|
|
438
|
+
export declare function commitCentralConfig(userDir: string): boolean;
|
|
397
439
|
/**
|
|
398
440
|
* True when the top-level `~/.agents/agents.yaml` carries a metadata header that
|
|
399
441
|
* is NOT the canonical {@link META_HEADER} — the P1 frozen-header case a box only
|
|
@@ -416,8 +458,13 @@ export declare function readTopLevelUserMeta(): Record<string, unknown> | null;
|
|
|
416
458
|
* writer that needs to read fresh state, decide, and commit within a SINGLE lock
|
|
417
459
|
* acquisition (e.g. browser tombstone eviction) can do so without the
|
|
418
460
|
* read-snapshot-then-separately-lock race that {@link updateMeta} would impose.
|
|
461
|
+
*
|
|
462
|
+
* Returns whether the central `agents.yaml` bytes actually changed, so the
|
|
463
|
+
* caller can commit it once — {@link commitCentralConfig} — AFTER releasing the
|
|
464
|
+
* meta lock (the git subprocesses must not run inside the short, non-heartbeated
|
|
465
|
+
* lockfile window). This function never commits.
|
|
419
466
|
*/
|
|
420
|
-
export declare function writeMetaUnlocked(meta: Meta):
|
|
467
|
+
export declare function writeMetaUnlocked(meta: Meta): boolean;
|
|
421
468
|
/**
|
|
422
469
|
* Read and cache ~/.agents/agents.yaml, migrating from legacy locations if needed.
|
|
423
470
|
*
|
|
@@ -433,6 +480,16 @@ export declare function readMeta(): Meta;
|
|
|
433
480
|
export declare function writeMeta(meta: Meta): void;
|
|
434
481
|
/** Update agents.yaml under lock and return the new state. */
|
|
435
482
|
export declare function updateMeta(updates: Partial<Meta> | ((meta: Meta) => Meta)): Meta;
|
|
483
|
+
/**
|
|
484
|
+
* Commit-on-write, invoked AFTER {@link withMetaLock} releases so the git
|
|
485
|
+
* subprocesses never run inside the short, non-heartbeated meta-lock window.
|
|
486
|
+
* A CLI command that actually moved the fleet-shared central agents.yaml commits
|
|
487
|
+
* it so the tree is never left dirty on that file at rest — the window that
|
|
488
|
+
* trips `dirtyTreeRefusal` and wedges pulls fleet-wide (PHNX-3968). Gated on a
|
|
489
|
+
* real byte change and never run in the daemon, whose publish tick owns central
|
|
490
|
+
* commits. See {@link commitCentralConfig}.
|
|
491
|
+
*/
|
|
492
|
+
export declare function commitCentralConfigAfterWrite(centralChanged: boolean): void;
|
|
436
493
|
/** Derive a filesystem-safe local clone path for a package source URL. */
|
|
437
494
|
export declare function getPackageLocalPath(source: string): string;
|
|
438
495
|
import type { AgentId, ResourceType, VersionResources, ResourcePattern } from './types.js';
|
package/dist/lib/state.js
CHANGED
|
@@ -642,6 +642,21 @@ export function getRuntimeStateDir() { return process.env.AGENTS_STATE_DIR ?? RU
|
|
|
642
642
|
export function getCompanionDir() { return COMPANION_CACHE_DIR; }
|
|
643
643
|
/** Path to browser runtime data — chrome-data, pids (~/.agents/.cache/browser/). */
|
|
644
644
|
export function getBrowserRuntimeDir() { return BROWSER_RUNTIME_DIR; }
|
|
645
|
+
/**
|
|
646
|
+
* Path to DURABLE browser-profile data (~/.agents/.history/browser-profiles/).
|
|
647
|
+
*
|
|
648
|
+
* This is the persistent home for an attach-only profile's `--user-data-dir` —
|
|
649
|
+
* where a one-time browser sign-in lives. It sits under `.history` (durable),
|
|
650
|
+
* NOT `.cache` (regenerable), for two reasons the ticket (PHNX-3967) named:
|
|
651
|
+
* - `agents browser profiles remove` sweeps `~/.agents/.cache/browser/<name>*`;
|
|
652
|
+
* a durable dir here survives that so logins are not wiped by a routine cleanup.
|
|
653
|
+
* - A cache wipe or the daemon reaper never touches it, so a signed-in Comet
|
|
654
|
+
* survives quit+relaunch.
|
|
655
|
+
* The user's canonical Comet is launched with this as `--user-data-dir`, and the
|
|
656
|
+
* ownership guard in the local driver compares the running instance's
|
|
657
|
+
* `--user-data-dir` against it to reject a foreign port-squatter.
|
|
658
|
+
*/
|
|
659
|
+
export function getBrowserDurableDir() { return path.join(HISTORY_DIR, 'browser-profiles'); }
|
|
645
660
|
/** Path to helper subprocess scratch (~/.agents/.cache/helpers/). */
|
|
646
661
|
export function getHelpersDir() { return HELPERS_DIR; }
|
|
647
662
|
/**
|
|
@@ -835,7 +850,9 @@ export function withMetaLock(fn) {
|
|
|
835
850
|
});
|
|
836
851
|
}
|
|
837
852
|
/** Atomic write only when the on-disk content differs — avoids needless mtime
|
|
838
|
-
* bumps (which would thrash the meta cache) on no-op field routing.
|
|
853
|
+
* bumps (which would thrash the meta cache) on no-op field routing. Returns
|
|
854
|
+
* whether it actually wrote, so a caller can react only to a real change (e.g.
|
|
855
|
+
* commit the central agents.yaml exactly when its bytes moved). */
|
|
839
856
|
function writeIfChanged(filePath, content) {
|
|
840
857
|
let current = null;
|
|
841
858
|
try {
|
|
@@ -843,8 +860,74 @@ function writeIfChanged(filePath, content) {
|
|
|
843
860
|
}
|
|
844
861
|
catch { /* absent */ }
|
|
845
862
|
if (current === content)
|
|
846
|
-
return;
|
|
863
|
+
return false;
|
|
847
864
|
atomicWriteFileSync(filePath, content);
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* True in the always-on daemon process (launched as `agents __daemon-run`,
|
|
869
|
+
* see cli/src/index.ts). The daemon owns central git commits through
|
|
870
|
+
* fleet-shared-repo-sync's publish tick, so a central write from inside the
|
|
871
|
+
* daemon must NOT also commit here — that would race the publisher's own
|
|
872
|
+
* add/commit/rebase/push on the same index. Every ordinary CLI invocation
|
|
873
|
+
* returns false and commits its own central edit synchronously.
|
|
874
|
+
*/
|
|
875
|
+
function isDaemonProcess() {
|
|
876
|
+
return process.argv[2] === '__daemon-run';
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Commit the central `agents.yaml` in the user repo, synchronously, so a CLI
|
|
880
|
+
* config mutation never leaves the working tree dirty on that one shared-line
|
|
881
|
+
* file at rest.
|
|
882
|
+
*
|
|
883
|
+
* Why this exists: CLI config commands rewrite the fleet-shared central
|
|
884
|
+
* agents.yaml as a plain file write. Left uncommitted, the tree is dirty on
|
|
885
|
+
* agents.yaml between the write and the daemon's next 15-min publish tick — and
|
|
886
|
+
* a peer's incoming publish commit (which also touches agents.yaml) then trips
|
|
887
|
+
* `dirtyTreeRefusal` ("incoming changes touch uncommitted paths: agents.yaml"),
|
|
888
|
+
* wedging `agents repo pull` fleet-wide (PHNX-3968). Committing the central edit
|
|
889
|
+
* in the same command that made it closes that window: agents.yaml is clean at
|
|
890
|
+
* rest, so nothing incoming can collide with it. Called AFTER the meta lock
|
|
891
|
+
* releases (see {@link commitCentralConfigAfterWrite}) so the git subprocesses
|
|
892
|
+
* never run inside the short, non-heartbeated lockfile window; the user repo
|
|
893
|
+
* already pushes.
|
|
894
|
+
*
|
|
895
|
+
* Scoped tightly: only called when the central bytes actually changed, and never
|
|
896
|
+
* from the daemon (see {@link isDaemonProcess}). A commit failure fails open —
|
|
897
|
+
* a concurrent daemon holding `index.lock`, a mid-rebase repo — leaving
|
|
898
|
+
* agents.yaml dirty; the next successful central write commits it, and until
|
|
899
|
+
* then a pull that would collide refuses rather than losing data. A config
|
|
900
|
+
* command must never fail because git hiccuped.
|
|
901
|
+
*
|
|
902
|
+
* Returns whether a commit was created. Exported for the real-repo tests.
|
|
903
|
+
*/
|
|
904
|
+
export function commitCentralConfig(userDir) {
|
|
905
|
+
const rel = 'agents.yaml';
|
|
906
|
+
try {
|
|
907
|
+
execFileSync('git', ['-C', userDir, 'rev-parse', '--is-inside-work-tree'], { stdio: 'ignore' });
|
|
908
|
+
}
|
|
909
|
+
catch {
|
|
910
|
+
return false; // plain ~/.agents with no git repo — leave it a loose write.
|
|
911
|
+
}
|
|
912
|
+
try {
|
|
913
|
+
execFileSync('git', ['-C', userDir, 'add', '--', rel], { stdio: 'ignore' });
|
|
914
|
+
// Nothing staged for agents.yaml (its bytes matched HEAD after all) → no
|
|
915
|
+
// empty commit. `diff --cached --quiet` exits 0 when the index equals HEAD
|
|
916
|
+
// for this path, 1 when it differs.
|
|
917
|
+
try {
|
|
918
|
+
execFileSync('git', ['-C', userDir, 'diff', '--cached', '--quiet', '--', rel], { stdio: 'ignore' });
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
catch { /* exit 1 → staged changes present, commit them */ }
|
|
922
|
+
// Pathspec-scoped commit: records ONLY agents.yaml even if other paths are
|
|
923
|
+
// staged, so a config write never sweeps unrelated staged work into its commit.
|
|
924
|
+
execFileSync('git', ['-C', userDir, '-c', 'commit.gpgsign=false', 'commit', '--no-verify',
|
|
925
|
+
'-m', 'chore(config): update agents.yaml', '--', rel], { stdio: 'ignore' });
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
catch {
|
|
929
|
+
return false; // fail open — see the doc comment.
|
|
930
|
+
}
|
|
848
931
|
}
|
|
849
932
|
/**
|
|
850
933
|
* Partition the in-memory Meta across four files by sync-domain:
|
|
@@ -1134,8 +1217,20 @@ function serializeCentral(central) {
|
|
|
1134
1217
|
* writer that needs to read fresh state, decide, and commit within a SINGLE lock
|
|
1135
1218
|
* acquisition (e.g. browser tombstone eviction) can do so without the
|
|
1136
1219
|
* read-snapshot-then-separately-lock race that {@link updateMeta} would impose.
|
|
1220
|
+
*
|
|
1221
|
+
* Returns whether the central `agents.yaml` bytes actually changed, so the
|
|
1222
|
+
* caller can commit it once — {@link commitCentralConfig} — AFTER releasing the
|
|
1223
|
+
* meta lock (the git subprocesses must not run inside the short, non-heartbeated
|
|
1224
|
+
* lockfile window). This function never commits.
|
|
1137
1225
|
*/
|
|
1138
1226
|
export function writeMetaUnlocked(meta) {
|
|
1227
|
+
const writesDeviceRoutines = Object.prototype.hasOwnProperty.call(meta, 'deviceRoutines');
|
|
1228
|
+
const writesDeviceConfig = Object.prototype.hasOwnProperty.call(meta, 'deviceConfig');
|
|
1229
|
+
const writesDeviceBrowser = Object.prototype.hasOwnProperty.call(meta, 'deviceBrowser');
|
|
1230
|
+
const writesDeviceFleet = Object.prototype.hasOwnProperty.call(meta, 'deviceFleet');
|
|
1231
|
+
const writesDeviceHosts = Object.prototype.hasOwnProperty.call(meta, 'deviceHosts');
|
|
1232
|
+
const writesDeviceAccounts = Object.prototype.hasOwnProperty.call(meta, 'deviceAccounts');
|
|
1233
|
+
const writesProjectRoot = Object.prototype.hasOwnProperty.call(meta, 'projectRoot');
|
|
1139
1234
|
// INVARIANT: every key destructured here must also be in BESPOKE_DEVICE_KEYS (and
|
|
1140
1235
|
// vice versa) — a bespoke device key that is classified but NOT pulled out here
|
|
1141
1236
|
// would fall into `central`, and the generic router skips it (BESPOKE_DEVICE_KEY_SET),
|
|
@@ -1191,17 +1286,17 @@ export function writeMetaUnlocked(meta) {
|
|
|
1191
1286
|
delete doc.isolatedAgents;
|
|
1192
1287
|
if (Array.isArray(deviceRoutines))
|
|
1193
1288
|
doc.routines = deviceRoutines;
|
|
1194
|
-
else
|
|
1289
|
+
else if (writesDeviceRoutines)
|
|
1195
1290
|
delete doc.routines;
|
|
1196
1291
|
const hasDeviceConfig = !!deviceConfig && Object.keys(deviceConfig).length > 0;
|
|
1197
1292
|
if (hasDeviceConfig)
|
|
1198
1293
|
doc.config = deviceConfig;
|
|
1199
|
-
else
|
|
1294
|
+
else if (writesDeviceConfig)
|
|
1200
1295
|
delete doc.config;
|
|
1201
1296
|
const hasDeviceBrowser = !!deviceBrowser && Object.keys(deviceBrowser).length > 0;
|
|
1202
1297
|
if (hasDeviceBrowser)
|
|
1203
1298
|
doc.browser = deviceBrowser;
|
|
1204
|
-
else
|
|
1299
|
+
else if (writesDeviceBrowser)
|
|
1205
1300
|
delete doc.browser;
|
|
1206
1301
|
// PHNX-3315 device-scoped fleet/hosts/accounts blocks. Each is this box's OWN
|
|
1207
1302
|
// slice; the effective fleet view is unioned across every device doc at read
|
|
@@ -1219,12 +1314,12 @@ export function writeMetaUnlocked(meta) {
|
|
|
1219
1314
|
df.ignored = fleetIgnored;
|
|
1220
1315
|
doc.fleet = df;
|
|
1221
1316
|
}
|
|
1222
|
-
else
|
|
1317
|
+
else if (writesDeviceFleet)
|
|
1223
1318
|
delete doc.fleet;
|
|
1224
1319
|
const hasDeviceHosts = !!deviceHosts && Object.keys(deviceHosts).length > 0;
|
|
1225
1320
|
if (hasDeviceHosts)
|
|
1226
1321
|
doc.hosts = deviceHosts;
|
|
1227
|
-
else
|
|
1322
|
+
else if (writesDeviceHosts)
|
|
1228
1323
|
delete doc.hosts;
|
|
1229
1324
|
const accountsNative = deviceAccounts?.native && Object.keys(deviceAccounts.native).length > 0
|
|
1230
1325
|
? deviceAccounts.native : undefined;
|
|
@@ -1238,12 +1333,12 @@ export function writeMetaUnlocked(meta) {
|
|
|
1238
1333
|
da.bindings = accountsBindings;
|
|
1239
1334
|
doc.accounts = da;
|
|
1240
1335
|
}
|
|
1241
|
-
else
|
|
1336
|
+
else if (writesDeviceAccounts)
|
|
1242
1337
|
delete doc.accounts;
|
|
1243
1338
|
const hasProjectRoot = typeof projectRoot === 'string' && projectRoot.length > 0;
|
|
1244
1339
|
if (hasProjectRoot)
|
|
1245
1340
|
doc.projectRoot = projectRoot;
|
|
1246
|
-
else
|
|
1341
|
+
else if (writesProjectRoot)
|
|
1247
1342
|
delete doc.projectRoot;
|
|
1248
1343
|
// Generic device-scoped keys (PHNX-3315): any key left in `central` that this
|
|
1249
1344
|
// version classifies as device but does NOT bespoke-route round-trips through
|
|
@@ -1284,8 +1379,9 @@ export function writeMetaUnlocked(meta) {
|
|
|
1284
1379
|
fs.mkdirSync(path.dirname(vrPath), { recursive: true });
|
|
1285
1380
|
writeIfChanged(vrPath, JSON.stringify(versions, null, 2) + '\n');
|
|
1286
1381
|
}
|
|
1287
|
-
writeIfChanged(META_FILE, serializeCentral(central));
|
|
1382
|
+
const centralChanged = writeIfChanged(META_FILE, serializeCentral(central));
|
|
1288
1383
|
metaCache = null;
|
|
1384
|
+
return centralChanged;
|
|
1289
1385
|
}
|
|
1290
1386
|
/**
|
|
1291
1387
|
* Overlay this machine's local state onto a central-portable Meta:
|
|
@@ -1471,7 +1567,15 @@ export function readMeta() {
|
|
|
1471
1567
|
if (parsed.registries) {
|
|
1472
1568
|
meta.registries = parsed.registries;
|
|
1473
1569
|
}
|
|
1474
|
-
|
|
1570
|
+
// Lock-safe, commit-free write: withMetaLock is reentrant (see
|
|
1571
|
+
// metaLockDepth), so this writes under the lock when called standalone and
|
|
1572
|
+
// is a no-op re-entry when readMeta runs inside updateMeta/writeMeta's held
|
|
1573
|
+
// lock — and writeMetaUnlocked never spawns git. Calling the PUBLIC
|
|
1574
|
+
// writeMeta here would run commit-on-write's git subprocess inside a held,
|
|
1575
|
+
// non-heartbeated lock. This one-shot legacy migration needs no synchronous
|
|
1576
|
+
// commit: it self-heals on the daemon's next publish tick or the next real
|
|
1577
|
+
// CLI central write.
|
|
1578
|
+
withMetaLock(() => writeMetaUnlocked(meta));
|
|
1475
1579
|
try {
|
|
1476
1580
|
fs.unlinkSync(oldMetaFile);
|
|
1477
1581
|
}
|
|
@@ -1525,18 +1629,36 @@ export function readMeta() {
|
|
|
1525
1629
|
}
|
|
1526
1630
|
/** Serialize and write agents.yaml to the user repo, invalidating the in-memory cache. */
|
|
1527
1631
|
export function writeMeta(meta) {
|
|
1528
|
-
withMetaLock(() => writeMetaUnlocked(meta));
|
|
1632
|
+
const centralChanged = withMetaLock(() => writeMetaUnlocked(meta));
|
|
1633
|
+
commitCentralConfigAfterWrite(centralChanged);
|
|
1529
1634
|
}
|
|
1530
1635
|
/** Update agents.yaml under lock and return the new state. */
|
|
1531
1636
|
export function updateMeta(updates) {
|
|
1532
|
-
|
|
1637
|
+
let centralChanged = false;
|
|
1638
|
+
const newMeta = withMetaLock(() => {
|
|
1533
1639
|
const meta = readMeta();
|
|
1534
|
-
const
|
|
1640
|
+
const nm = typeof updates === 'function'
|
|
1535
1641
|
? updates(meta)
|
|
1536
1642
|
: { ...meta, ...updates };
|
|
1537
|
-
writeMetaUnlocked(
|
|
1538
|
-
return
|
|
1643
|
+
centralChanged = writeMetaUnlocked(nm);
|
|
1644
|
+
return nm;
|
|
1539
1645
|
});
|
|
1646
|
+
commitCentralConfigAfterWrite(centralChanged);
|
|
1647
|
+
return newMeta;
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Commit-on-write, invoked AFTER {@link withMetaLock} releases so the git
|
|
1651
|
+
* subprocesses never run inside the short, non-heartbeated meta-lock window.
|
|
1652
|
+
* A CLI command that actually moved the fleet-shared central agents.yaml commits
|
|
1653
|
+
* it so the tree is never left dirty on that file at rest — the window that
|
|
1654
|
+
* trips `dirtyTreeRefusal` and wedges pulls fleet-wide (PHNX-3968). Gated on a
|
|
1655
|
+
* real byte change and never run in the daemon, whose publish tick owns central
|
|
1656
|
+
* commits. See {@link commitCentralConfig}.
|
|
1657
|
+
*/
|
|
1658
|
+
export function commitCentralConfigAfterWrite(centralChanged) {
|
|
1659
|
+
if (centralChanged && !isDaemonProcess()) {
|
|
1660
|
+
commitCentralConfig(USER_AGENTS_DIR);
|
|
1661
|
+
}
|
|
1540
1662
|
}
|
|
1541
1663
|
/** Derive a filesystem-safe local clone path for a package source URL. */
|
|
1542
1664
|
export function getPackageLocalPath(source) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-summarizer configuration (PHNX-3939).
|
|
3
|
+
*
|
|
4
|
+
* Resolves the three knobs behind the daemon summarizer — enabled / base URL /
|
|
5
|
+
* model — from `agents config` (`summarizer.*`, user-scope in the central
|
|
6
|
+
* agents.yaml) with a per-process env override
|
|
7
|
+
* (`AGENTS_SUMMARIZER_ENABLED` / `AGENTS_SUMMARIZER_BASEURL` /
|
|
8
|
+
* `AGENTS_SUMMARIZER_MODEL`). Off by default: with no config and no env, the
|
|
9
|
+
* summarizer is disabled and makes zero model calls.
|
|
10
|
+
*
|
|
11
|
+
* `isSummarizerReady()` is memoized on a short TTL because the display merge
|
|
12
|
+
* (the watch-stream projections) calls it once per session row — a fresh
|
|
13
|
+
* agents.yaml read per row would defeat the "blazing fast" requirement.
|
|
14
|
+
*/
|
|
15
|
+
export interface SummarizerConfig {
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
/** Anthropic-wire base URL (Ollama/vLLM/LiteLLM), or undefined when unconfigured. */
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
/** Model id to request, or undefined when unconfigured. */
|
|
20
|
+
model?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the full summarizer config. Env overrides the stored config key by key;
|
|
24
|
+
* an unset env var falls through to `agents config`, then to the built-in
|
|
25
|
+
* default (disabled). Never throws — a missing/corrupt config reads as unset.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveSummarizerConfig(env?: NodeJS.ProcessEnv): SummarizerConfig;
|
|
28
|
+
/**
|
|
29
|
+
* True only when the summarizer is enabled AND has a base URL + model to call.
|
|
30
|
+
* A configuration that is `enabled` but missing an endpoint cannot produce a
|
|
31
|
+
* summary, so it is treated as unconfigured (the service no-ops, the merge marks
|
|
32
|
+
* `skipped`) rather than erroring on every tick.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isSummarizerRunnable(config: SummarizerConfig): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Memoized "will a summary actually be produced?" check for the hot merge path.
|
|
37
|
+
* Reflects {@link isSummarizerRunnable} — enabled AND a base URL AND a model —
|
|
38
|
+
* NOT just `enabled`, because an enabled-but-unconfigured summarizer computes
|
|
39
|
+
* nothing, so a row with no cached summary must read `skipped`, not a `pending`
|
|
40
|
+
* that never resolves (the exact case: `summarizer.enabled on` set before the
|
|
41
|
+
* endpoint). TTL keeps a config change visible within a few seconds without a
|
|
42
|
+
* per-row agents.yaml read.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isSummarizerReady(nowMs?: number): boolean;
|
|
45
|
+
/** Test seam: drop the memoized ready flag so the next read re-resolves. */
|
|
46
|
+
export declare function resetSummarizerReadyCacheForTest(): void;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-summarizer configuration (PHNX-3939).
|
|
3
|
+
*
|
|
4
|
+
* Resolves the three knobs behind the daemon summarizer — enabled / base URL /
|
|
5
|
+
* model — from `agents config` (`summarizer.*`, user-scope in the central
|
|
6
|
+
* agents.yaml) with a per-process env override
|
|
7
|
+
* (`AGENTS_SUMMARIZER_ENABLED` / `AGENTS_SUMMARIZER_BASEURL` /
|
|
8
|
+
* `AGENTS_SUMMARIZER_MODEL`). Off by default: with no config and no env, the
|
|
9
|
+
* summarizer is disabled and makes zero model calls.
|
|
10
|
+
*
|
|
11
|
+
* `isSummarizerReady()` is memoized on a short TTL because the display merge
|
|
12
|
+
* (the watch-stream projections) calls it once per session row — a fresh
|
|
13
|
+
* agents.yaml read per row would defeat the "blazing fast" requirement.
|
|
14
|
+
*/
|
|
15
|
+
import { getConfigValue } from '../device-config.js';
|
|
16
|
+
/** Parse a boolean-ish env value; undefined when the var is unset. */
|
|
17
|
+
function envBool(raw) {
|
|
18
|
+
if (raw === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
const v = raw.trim().toLowerCase();
|
|
21
|
+
if (v === '1' || v === 'true' || v === 'on' || v === 'yes')
|
|
22
|
+
return true;
|
|
23
|
+
if (v === '0' || v === 'false' || v === 'off' || v === 'no' || v === '')
|
|
24
|
+
return false;
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function envString(raw) {
|
|
28
|
+
const v = raw?.trim();
|
|
29
|
+
return v ? v : undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the full summarizer config. Env overrides the stored config key by key;
|
|
33
|
+
* an unset env var falls through to `agents config`, then to the built-in
|
|
34
|
+
* default (disabled). Never throws — a missing/corrupt config reads as unset.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveSummarizerConfig(env = process.env) {
|
|
37
|
+
let storedEnabled;
|
|
38
|
+
let storedBaseUrl;
|
|
39
|
+
let storedModel;
|
|
40
|
+
try {
|
|
41
|
+
storedEnabled = getConfigValue('summarizer.enabled').value;
|
|
42
|
+
storedBaseUrl = getConfigValue('summarizer.baseUrl').value;
|
|
43
|
+
storedModel = getConfigValue('summarizer.model').value;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// A missing/corrupt config store must not break the read path — treat as unset.
|
|
47
|
+
}
|
|
48
|
+
const enabled = envBool(env.AGENTS_SUMMARIZER_ENABLED) ?? storedEnabled ?? false;
|
|
49
|
+
const baseUrl = envString(env.AGENTS_SUMMARIZER_BASEURL) ?? envString(storedBaseUrl);
|
|
50
|
+
const model = envString(env.AGENTS_SUMMARIZER_MODEL) ?? envString(storedModel);
|
|
51
|
+
return { enabled, baseUrl, model };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* True only when the summarizer is enabled AND has a base URL + model to call.
|
|
55
|
+
* A configuration that is `enabled` but missing an endpoint cannot produce a
|
|
56
|
+
* summary, so it is treated as unconfigured (the service no-ops, the merge marks
|
|
57
|
+
* `skipped`) rather than erroring on every tick.
|
|
58
|
+
*/
|
|
59
|
+
export function isSummarizerRunnable(config) {
|
|
60
|
+
return config.enabled && Boolean(config.baseUrl) && Boolean(config.model);
|
|
61
|
+
}
|
|
62
|
+
let cachedReady = null;
|
|
63
|
+
const READY_TTL_MS = 3_000;
|
|
64
|
+
/**
|
|
65
|
+
* Memoized "will a summary actually be produced?" check for the hot merge path.
|
|
66
|
+
* Reflects {@link isSummarizerRunnable} — enabled AND a base URL AND a model —
|
|
67
|
+
* NOT just `enabled`, because an enabled-but-unconfigured summarizer computes
|
|
68
|
+
* nothing, so a row with no cached summary must read `skipped`, not a `pending`
|
|
69
|
+
* that never resolves (the exact case: `summarizer.enabled on` set before the
|
|
70
|
+
* endpoint). TTL keeps a config change visible within a few seconds without a
|
|
71
|
+
* per-row agents.yaml read.
|
|
72
|
+
*/
|
|
73
|
+
export function isSummarizerReady(nowMs = Date.now()) {
|
|
74
|
+
if (cachedReady && nowMs - cachedReady.at < READY_TTL_MS)
|
|
75
|
+
return cachedReady.value;
|
|
76
|
+
const value = isSummarizerRunnable(resolveSummarizerConfig());
|
|
77
|
+
cachedReady = { at: nowMs, value };
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
/** Test seam: drop the memoized ready flag so the next read re-resolves. */
|
|
81
|
+
export function resetSummarizerReadyCacheForTest() {
|
|
82
|
+
cachedReady = null;
|
|
83
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One session-summarizer pass (PHNX-3939) — the work the background
|
|
3
|
+
* SessionSummarizerService does per tick, factored out so it is testable with an
|
|
4
|
+
* injected model stub and no daemon scaffolding.
|
|
5
|
+
*
|
|
6
|
+
* Off the request path by construction: it reads THIS box's already-published
|
|
7
|
+
* live sessions (the warm cache the session-state service writes — never a fresh
|
|
8
|
+
* gather) and, per session, reuses the transcript-keyed `session_summaries` row
|
|
9
|
+
* until the transcript bytes change, so an unchanged session costs one indexed
|
|
10
|
+
* read and zero model calls. When the summarizer is disabled or has no endpoint,
|
|
11
|
+
* the pass does nothing at all.
|
|
12
|
+
*/
|
|
13
|
+
import type { ActiveSession } from '../session/active.js';
|
|
14
|
+
import { type SummarizerConfig } from './config.js';
|
|
15
|
+
import { summarize as defaultSummarize } from './summarize.js';
|
|
16
|
+
/** Max sessions summarized per tick — a debounce ceiling on local-model calls. */
|
|
17
|
+
export declare const SUMMARIZER_MAX_PER_TICK = 8;
|
|
18
|
+
export interface SummarizerPassOptions {
|
|
19
|
+
now?: number;
|
|
20
|
+
config?: SummarizerConfig;
|
|
21
|
+
/** Live sessions to consider; default = this box's warm local-session cache. */
|
|
22
|
+
sessions?: ActiveSession[];
|
|
23
|
+
/** Injectable model call (tests stub the endpoint here). */
|
|
24
|
+
summarizeImpl?: typeof defaultSummarize;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
maxPerTick?: number;
|
|
27
|
+
/** Injectable stat for tests; defaults to fs.statSync. */
|
|
28
|
+
statFile?: (p: string) => {
|
|
29
|
+
mtimeMs: number;
|
|
30
|
+
size: number;
|
|
31
|
+
};
|
|
32
|
+
/** Skip the reader-recency gate (tests pass sessions directly). */
|
|
33
|
+
requireReader?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface SummarizerPassResult {
|
|
36
|
+
disabled: boolean;
|
|
37
|
+
computed: number;
|
|
38
|
+
reused: number;
|
|
39
|
+
skipped: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Run one pass. Returns per-outcome counts. Never throws — a per-session failure
|
|
43
|
+
* is isolated so one bad transcript can't stall the whole tick.
|
|
44
|
+
*/
|
|
45
|
+
export declare function runSummarizerPass(opts?: SummarizerPassOptions): Promise<SummarizerPassResult>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One session-summarizer pass (PHNX-3939) — the work the background
|
|
3
|
+
* SessionSummarizerService does per tick, factored out so it is testable with an
|
|
4
|
+
* injected model stub and no daemon scaffolding.
|
|
5
|
+
*
|
|
6
|
+
* Off the request path by construction: it reads THIS box's already-published
|
|
7
|
+
* live sessions (the warm cache the session-state service writes — never a fresh
|
|
8
|
+
* gather) and, per session, reuses the transcript-keyed `session_summaries` row
|
|
9
|
+
* until the transcript bytes change, so an unchanged session costs one indexed
|
|
10
|
+
* read and zero model calls. When the summarizer is disabled or has no endpoint,
|
|
11
|
+
* the pass does nothing at all.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import { readSessionSummary, readSessionSummaryAny, writeSessionSummary, } from '../session/db.js';
|
|
15
|
+
import { isActiveSessionsJournalReaderRecent, readActiveSessionsCache, } from '../session/session-cache.js';
|
|
16
|
+
import { resolveSummarizerConfig, isSummarizerRunnable } from './config.js';
|
|
17
|
+
import { summarize as defaultSummarize } from './summarize.js';
|
|
18
|
+
/** Max sessions summarized per tick — a debounce ceiling on local-model calls. */
|
|
19
|
+
export const SUMMARIZER_MAX_PER_TICK = 8;
|
|
20
|
+
/**
|
|
21
|
+
* Merge model checkpoint TEXTS with any prior stored checkpoints so a line that
|
|
22
|
+
* already existed keeps its original `at`, and only genuinely new lines get
|
|
23
|
+
* stamped `now`. Order follows the model output (newest last).
|
|
24
|
+
*/
|
|
25
|
+
function stampCheckpoints(texts, prior, nowIso) {
|
|
26
|
+
const priorAt = new Map((prior ?? []).map((c) => [c.text, c.at]));
|
|
27
|
+
return texts.map((text) => ({ text, at: priorAt.get(text) ?? nowIso }));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Run one pass. Returns per-outcome counts. Never throws — a per-session failure
|
|
31
|
+
* is isolated so one bad transcript can't stall the whole tick.
|
|
32
|
+
*/
|
|
33
|
+
export async function runSummarizerPass(opts = {}) {
|
|
34
|
+
const now = opts.now ?? Date.now();
|
|
35
|
+
const config = opts.config ?? resolveSummarizerConfig();
|
|
36
|
+
const result = { disabled: false, computed: 0, reused: 0, skipped: 0 };
|
|
37
|
+
if (!isSummarizerRunnable(config)) {
|
|
38
|
+
result.disabled = true;
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
let sessions = opts.sessions;
|
|
42
|
+
if (!sessions) {
|
|
43
|
+
// Reader-gated like the active-sessions warm tick: no watcher, no work.
|
|
44
|
+
if (opts.requireReader !== false && !isActiveSessionsJournalReaderRecent(now))
|
|
45
|
+
return result;
|
|
46
|
+
sessions = readActiveSessionsCache('local')?.sessions ?? [];
|
|
47
|
+
}
|
|
48
|
+
const stat = opts.statFile ?? ((p) => {
|
|
49
|
+
const s = fs.statSync(p);
|
|
50
|
+
return { mtimeMs: s.mtimeMs, size: s.size };
|
|
51
|
+
});
|
|
52
|
+
const runSummarize = opts.summarizeImpl ?? defaultSummarize;
|
|
53
|
+
const maxPerTick = opts.maxPerTick ?? SUMMARIZER_MAX_PER_TICK;
|
|
54
|
+
const nowIso = new Date(now).toISOString();
|
|
55
|
+
let budget = maxPerTick;
|
|
56
|
+
for (const s of sessions) {
|
|
57
|
+
if (opts.signal?.aborted)
|
|
58
|
+
break;
|
|
59
|
+
if (budget <= 0)
|
|
60
|
+
break;
|
|
61
|
+
const id = s.sessionId;
|
|
62
|
+
const file = s.sessionFile;
|
|
63
|
+
if (!id || !file)
|
|
64
|
+
continue;
|
|
65
|
+
let stamp;
|
|
66
|
+
try {
|
|
67
|
+
const st = stat(file);
|
|
68
|
+
stamp = { fileMtimeMs: Math.round(st.mtimeMs), fileSize: st.size };
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
continue; // transcript unreadable this tick — try again next time
|
|
72
|
+
}
|
|
73
|
+
// Cached for these exact bytes → never recompute (the core "blazing fast" rule).
|
|
74
|
+
if (readSessionSummary(id, stamp)) {
|
|
75
|
+
result.reused++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
budget--;
|
|
79
|
+
const prompt = (s.firstUserMessage ?? s.topic ?? '').trim();
|
|
80
|
+
const prior = readSessionSummaryAny(id);
|
|
81
|
+
if (!prompt) {
|
|
82
|
+
// No extractable intent yet — cache a skip against these bytes so we don't
|
|
83
|
+
// re-attempt every tick; a later transcript write (new bytes) retries.
|
|
84
|
+
writeSessionSummary({ id, ...stamp, summary: { summaryState: 'skipped' } });
|
|
85
|
+
result.skipped++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
let computed;
|
|
89
|
+
try {
|
|
90
|
+
computed = await runSummarize(prompt, { todos: s.todos, plan: s.plan, phase: s.phase }, { baseUrl: config.baseUrl, model: config.model, ...(opts.signal ? { signal: opts.signal } : {}) });
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
computed = undefined;
|
|
94
|
+
}
|
|
95
|
+
if (!computed) {
|
|
96
|
+
writeSessionSummary({ id, ...stamp, summary: { summaryState: 'skipped' } });
|
|
97
|
+
result.skipped++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const entry = {
|
|
101
|
+
// Goal is computed ONCE at first sight — a prior goal is kept across
|
|
102
|
+
// progress deltas so it stays stable while checkpoints/checklist refresh.
|
|
103
|
+
goal: prior?.goal ?? computed.goal,
|
|
104
|
+
checkpoints: stampCheckpoints(computed.checkpoints, prior?.checkpoints, nowIso),
|
|
105
|
+
summaryChecklist: computed.checklist,
|
|
106
|
+
summaryState: 'ready',
|
|
107
|
+
};
|
|
108
|
+
writeSessionSummary({ id, ...stamp, summary: entry });
|
|
109
|
+
result.computed++;
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|