peaks-loop 4.0.7 → 4.0.9
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 +58 -288
- package/dist/cli/commands/_register.js +2 -0
- package/dist/cli/commands/baseline-commands.d.ts +3 -0
- package/dist/cli/commands/baseline-commands.js +146 -0
- package/dist/cli/commands/code-job-shape-commands.js +8 -11
- package/dist/cli/commands/code-mode-gate-commands.js +1 -43
- package/dist/cli/commands/code-runtime-commands.js +36 -14
- package/dist/cli/commands/core/skill-command.js +75 -13
- package/dist/cli/commands/core/standards-command.js +5 -50
- package/dist/cli/commands/dispatch-commands.d.ts +20 -0
- package/dist/cli/commands/dispatch-commands.js +39 -38
- package/dist/cli/commands/dispatch-from-dag.js +17 -0
- package/dist/cli/commands/heartbeat-commands.d.ts +21 -0
- package/dist/cli/commands/heartbeat-commands.js +40 -0
- package/dist/cli/commands/hook-handle.js +5 -2
- package/dist/cli/commands/hooks-commands.js +9 -26
- package/dist/cli/commands/loop-eval-commands.js +5 -2
- package/dist/cli/commands/statusline-commands.js +1 -1
- package/dist/cli/commands/sub-agent-commands.js +0 -2
- package/dist/cli/commands/sub-agent-shared.d.ts +7 -9
- package/dist/cli/commands/workflow-commands.js +8 -0
- package/dist/cli/commands/workflow-lifecycle-commands.d.ts +63 -0
- package/dist/cli/commands/workflow-lifecycle-commands.js +302 -0
- package/dist/cli/commands/workspace/init-command.js +19 -0
- package/dist/services/audit/backing-detector.d.ts +2 -19
- package/dist/services/audit/backing-detector.js +7 -66
- package/dist/services/audit/enforcers/active-skill-resolver.d.ts +25 -9
- package/dist/services/audit/enforcers/active-skill-resolver.js +83 -21
- package/dist/services/audit/enforcers/lint-audit-regression.js +1 -10
- package/dist/services/capability-audit-service/cross-check.d.ts +8 -0
- package/dist/services/capability-audit-service/cross-check.js +11 -0
- package/dist/services/capability-audit-service/index.d.ts +3 -0
- package/dist/services/capability-audit-service/index.js +2 -0
- package/dist/services/capability-audit-service/runner.d.ts +27 -0
- package/dist/services/capability-audit-service/runner.js +40 -0
- package/dist/services/capability-audit-service/staleness.d.ts +1 -0
- package/dist/services/capability-audit-service/staleness.js +7 -0
- package/dist/services/capability-audit-service/types.d.ts +24 -0
- package/dist/services/capability-audit-service/types.js +1 -0
- package/dist/services/capability-baseline/store.d.ts +33 -0
- package/dist/services/capability-baseline/store.js +80 -0
- package/dist/services/capability-baseline/types.d.ts +45 -0
- package/dist/services/capability-baseline/types.js +5 -0
- package/dist/services/capability-baseline/validator.d.ts +19 -0
- package/dist/services/capability-baseline/validator.js +38 -0
- package/dist/services/capability-guard-runner/contracts/J01.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J01.js +48 -0
- package/dist/services/capability-guard-runner/contracts/J02.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J02.js +34 -0
- package/dist/services/capability-guard-runner/contracts/J03.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J03.js +24 -0
- package/dist/services/capability-guard-runner/contracts/J04.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J04.js +40 -0
- package/dist/services/capability-guard-runner/contracts/J05.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J05.js +21 -0
- package/dist/services/capability-guard-runner/contracts/J06.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J06.js +43 -0
- package/dist/services/capability-guard-runner/contracts/J07.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J07.js +53 -0
- package/dist/services/capability-guard-runner/contracts/J08.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J08.js +43 -0
- package/dist/services/capability-guard-runner/contracts/J09.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J09.js +44 -0
- package/dist/services/capability-guard-runner/contracts/J10.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J10.js +40 -0
- package/dist/services/capability-guard-runner/contracts/J11.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J11.js +39 -0
- package/dist/services/capability-guard-runner/contracts/J12.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J12.js +34 -0
- package/dist/services/capability-guard-runner/contracts/J13.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J13.js +45 -0
- package/dist/services/capability-guard-runner/contracts/J14.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J14.js +36 -0
- package/dist/services/capability-guard-runner/contracts/J15.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J15.js +40 -0
- package/dist/services/capability-guard-runner/diff.d.ts +2 -0
- package/dist/services/capability-guard-runner/diff.js +7 -0
- package/dist/services/capability-guard-runner/runner.d.ts +9 -0
- package/dist/services/capability-guard-runner/runner.js +22 -0
- package/dist/services/capability-guard-runner/types.d.ts +39 -0
- package/dist/services/capability-guard-runner/types.js +1 -0
- package/dist/services/code/auto-compact-orchestrator.d.ts +35 -2
- package/dist/services/code/auto-compact-orchestrator.js +29 -9
- package/dist/services/context/build-dispatch-system-prompt.js +0 -18
- package/dist/services/dispatch/dispatch-record-writer.d.ts +25 -1
- package/dist/services/dispatch/dispatch-record-writer.js +85 -15
- package/dist/services/dispatch/test-tool-detection.d.ts +0 -14
- package/dist/services/dispatch/test-tool-detection.js +0 -22
- package/dist/services/doctor/doctor-service/checks/skill-presence.d.ts +8 -0
- package/dist/services/doctor/doctor-service/checks/skill-presence.js +9 -1
- package/dist/services/evolution/evolution-types.d.ts +16 -16
- package/dist/services/final-review/final-review-service.d.ts +8 -0
- package/dist/services/final-review/final-review-service.js +14 -0
- package/dist/services/final-review/final-review-types.d.ts +1 -1
- package/dist/services/hooks/presence-marker-detector.d.ts +6 -0
- package/dist/services/hooks/presence-marker-detector.js +48 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +20 -0
- package/dist/services/ide/adapters/codex-adapter.js +20 -1
- package/dist/services/ide/adapters/cursor-adapter.js +21 -1
- package/dist/services/ide/adapters/hermes-adapter.js +20 -1
- package/dist/services/ide/adapters/openclaw-adapter.js +20 -1
- package/dist/services/ide/adapters/qoder-adapter.js +20 -1
- package/dist/services/ide/adapters/tongyi-lingma-adapter.js +20 -1
- package/dist/services/ide/adapters/trae-adapter.js +21 -1
- package/dist/services/ide/adapters/zcode-adapter.js +19 -0
- package/dist/services/ide/ide-types.d.ts +15 -0
- package/dist/services/job/job-progress-store.d.ts +2 -2
- package/dist/services/job/job-types.d.ts +26 -26
- package/dist/services/observability/observability-service.d.ts +4 -4
- package/dist/services/scan/archetype-service.js +2 -52
- package/dist/services/sediment/json-schema.d.ts +4 -4
- package/dist/services/session/caller-binding-service.d.ts +21 -0
- package/dist/services/session/caller-binding-service.js +35 -0
- package/dist/services/session/caller-id-types.d.ts +62 -13
- package/dist/services/session/caller-id-types.js +25 -13
- package/dist/services/session/index.d.ts +4 -2
- package/dist/services/session/index.js +8 -2
- package/dist/services/session/platform-fallbacks.d.ts +25 -17
- package/dist/services/session/platform-fallbacks.js +26 -28
- package/dist/services/session/resolve-caller-id.d.ts +64 -29
- package/dist/services/session/resolve-caller-id.js +122 -48
- package/dist/services/session/session-manager.d.ts +0 -29
- package/dist/services/session/session-manager.js +0 -33
- package/dist/services/session/session-resume-service.d.ts +4 -0
- package/dist/services/session/session-resume-service.js +4 -0
- package/dist/services/skills/hooks-settings-service.d.ts +0 -41
- package/dist/services/skills/hooks-settings-service.js +5 -61
- package/dist/services/skills/presence-lease-service.d.ts +73 -0
- package/dist/services/skills/presence-lease-service.js +358 -0
- package/dist/services/skills/presence-lease-types.d.ts +75 -0
- package/dist/services/skills/presence-lease-types.js +16 -0
- package/dist/services/skills/skill-presence-service.js +86 -22
- package/dist/services/skills/skill-statusline-renderer.d.ts +46 -56
- package/dist/services/skills/skill-statusline-renderer.js +312 -100
- package/dist/services/slice/slice-check-service.js +0 -59
- package/dist/services/standards/project-standards-service.js +0 -44
- package/dist/services/workflow/workflow-graph-store.d.ts +77 -0
- package/dist/services/workflow/workflow-graph-store.js +278 -0
- package/dist/services/workflow/workflow-graph-types.d.ts +56 -0
- package/dist/services/workflow/workflow-graph-types.js +67 -0
- package/dist/services/workflow/workflow-inflight-probe.d.ts +52 -0
- package/dist/services/workflow/workflow-inflight-probe.js +86 -0
- package/dist/services/workflow/workflow-node-lifecycle.d.ts +67 -0
- package/dist/services/workflow/workflow-node-lifecycle.js +348 -0
- package/dist/services/workflow/workflow-presence-lifecycle.d.ts +58 -0
- package/dist/services/workflow/workflow-presence-lifecycle.js +196 -0
- package/dist/services/workspace/reconcile-service.d.ts +31 -0
- package/dist/services/workspace/reconcile-service.js +132 -1
- package/package.json +5 -4
- package/skills/peaks-code/SKILL.md +5 -43
- package/skills/peaks-code/references/completion-handoff.md +1 -1
- package/skills/peaks-code/references/skill-presence-and-title.md +1 -1
- package/dist/cli/commands/sub-agent-exec-command.d.ts +0 -25
- package/dist/cli/commands/sub-agent-exec-command.js +0 -132
- package/dist/services/standards/find-skills-integration.d.ts +0 -59
- package/dist/services/standards/find-skills-integration.js +0 -225
|
@@ -57,14 +57,6 @@ function resolveIde(options) {
|
|
|
57
57
|
function resolveSkipProgress(options) {
|
|
58
58
|
return options?.skipProgress === true;
|
|
59
59
|
}
|
|
60
|
-
/** Slice 4.0.7-PR-meta-5: read the withEditEnforcement opt-in flag. */
|
|
61
|
-
function resolveWithEditEnforcement(options) {
|
|
62
|
-
return options?.withEditEnforcement === true;
|
|
63
|
-
}
|
|
64
|
-
/** Slice 4.0.7-PR-meta-5: read the withScopeCounter opt-in flag. */
|
|
65
|
-
function resolveWithScopeCounter(options) {
|
|
66
|
-
return options?.withScopeCounter === true;
|
|
67
|
-
}
|
|
68
60
|
/** Resolve settings root dir for a scope. */
|
|
69
61
|
function resolveSettingsRoot(scope, projectRoot) {
|
|
70
62
|
if (scope === 'global')
|
|
@@ -173,32 +165,6 @@ export function readInstalledEntriesFromSettings(settings, ide) {
|
|
|
173
165
|
}
|
|
174
166
|
return result;
|
|
175
167
|
}
|
|
176
|
-
/**
|
|
177
|
-
* Slice 4.0.7-PR-meta-1: the Edit-enforcement hook entry. Off by
|
|
178
|
-
* default; opt in via `peaks hooks install --with-edit-enforcement`
|
|
179
|
-
* (or `PEAKS_INSTALL_EDIT_ENFORCEMENT=1` env). The entry points at
|
|
180
|
-
* the new `pre-tool-edit-enforcement.sh` script in
|
|
181
|
-
* `src/services/hooks/`. The matcher targets Edit / Write / MultiEdit
|
|
182
|
-
* / NotebookEdit (Claude Code: "Edit|Write|MultiEdit|NotebookEdit";
|
|
183
|
-
* Trae: same). When the hook fires, the script blocks src/** edits
|
|
184
|
-
* without an RD artifact on disk (per slice 4.0.7-PR-meta-1 design).
|
|
185
|
-
*/
|
|
186
|
-
export const EDIT_ENFORCEMENT_HOOK_MATCHER = 'Edit|Write|MultiEdit|NotebookEdit';
|
|
187
|
-
export const EDIT_ENFORCEMENT_HOOK_SENTINEL = 'peaks edit enforcement';
|
|
188
|
-
export const EDIT_ENFORCEMENT_HOOK_EVENT = 'PreToolUse';
|
|
189
|
-
/**
|
|
190
|
-
* Slice 4.0.7-PR-meta-3: the scope-counter hook entry. Off by
|
|
191
|
-
* default. Tracks cumulative application-source edits in the
|
|
192
|
-
* active session and emits a fail-LOUD reflection reminder at
|
|
193
|
-
* thresholds 5, 10, 20. The LLM cannot continue without writing
|
|
194
|
-
* to `.peaks/_runtime/<sid>/scope-reflection.md` once a threshold
|
|
195
|
-
* is hit. Different from the harness-side SCOPE WARNING (which
|
|
196
|
-
* is informational noise); the meta-3 hook surfaces a checkpoint
|
|
197
|
-
* the LLM must reference in its next turn.
|
|
198
|
-
*/
|
|
199
|
-
export const SCOPE_COUNTER_HOOK_MATCHER = 'Edit|Write|MultiEdit|NotebookEdit';
|
|
200
|
-
export const SCOPE_COUNTER_HOOK_SENTINEL = 'peaks scope counter';
|
|
201
|
-
export const SCOPE_COUNTER_HOOK_EVENT = 'PreToolUse';
|
|
202
168
|
/**
|
|
203
169
|
* Compute the per-IDE peaks hook entries to merge into the settings file.
|
|
204
170
|
* Replaces the slice #1 hardcoded `PEAKS_HOOK_ENTRIES` constant; the constant
|
|
@@ -216,33 +182,11 @@ export const SCOPE_COUNTER_HOOK_EVENT = 'PreToolUse';
|
|
|
216
182
|
* can find + remove any stale progress-start entry that an older
|
|
217
183
|
* `peaks hooks install` may have written before this slice.
|
|
218
184
|
*/
|
|
219
|
-
function resolveHookEntries(ide,
|
|
185
|
+
function resolveHookEntries(ide, _skipProgress = false) {
|
|
220
186
|
const spec = resolveHookSpec(ide);
|
|
221
|
-
|
|
187
|
+
return [
|
|
222
188
|
{ sentinel: spec.hookEnforceSentinel, matcher: spec.hookEnforceMatcher, command: spec.hookEnforceCommand, event: spec.hookEnforceEvent }
|
|
223
189
|
];
|
|
224
|
-
// Slice 4.0.7-PR-meta-1: opt-in Edit enforcement. Default false
|
|
225
|
-
// to preserve the existing `peaks hooks install` behavior; users
|
|
226
|
-
// who want the LLM Edit layer enforced call install with the
|
|
227
|
-
// opt-in flag or env var.
|
|
228
|
-
if ('withEditEnforcement' in options && options.withEditEnforcement === true) {
|
|
229
|
-
entries.push({
|
|
230
|
-
sentinel: EDIT_ENFORCEMENT_HOOK_SENTINEL,
|
|
231
|
-
matcher: EDIT_ENFORCEMENT_HOOK_MATCHER,
|
|
232
|
-
command: 'peaks edit-enforcement',
|
|
233
|
-
event: EDIT_ENFORCEMENT_HOOK_EVENT
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
// Slice 4.0.7-PR-meta-3: opt-in scope counter. Default false.
|
|
237
|
-
if ('withScopeCounter' in options && options.withScopeCounter === true) {
|
|
238
|
-
entries.push({
|
|
239
|
-
sentinel: SCOPE_COUNTER_HOOK_SENTINEL,
|
|
240
|
-
matcher: SCOPE_COUNTER_HOOK_MATCHER,
|
|
241
|
-
command: 'peaks scope-counter',
|
|
242
|
-
event: SCOPE_COUNTER_HOOK_EVENT
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
return entries;
|
|
246
190
|
}
|
|
247
191
|
/**
|
|
248
192
|
* Legacy sentinel set used by uninstall + status to find and remove stale
|
|
@@ -449,7 +393,7 @@ export function planHookInstall(scope, projectRoot, options) {
|
|
|
449
393
|
};
|
|
450
394
|
}
|
|
451
395
|
/** Merge all peaks-managed hook entries into settings, preserving all other keys and hooks. */
|
|
452
|
-
function withHooksInstalledForIde(settings, ide,
|
|
396
|
+
function withHooksInstalledForIde(settings, ide, _skipProgress = false) {
|
|
453
397
|
const existingHooks = (settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks))
|
|
454
398
|
? settings.hooks
|
|
455
399
|
: {};
|
|
@@ -457,7 +401,7 @@ function withHooksInstalledForIde(settings, ide, options = {}) {
|
|
|
457
401
|
// beforeToolCall; Claude: gate-enforce on PreToolUse). Group by event
|
|
458
402
|
// so each event array is independently merged.
|
|
459
403
|
const ourByEvent = new Map();
|
|
460
|
-
for (const spec of resolveHookEntries(ide
|
|
404
|
+
for (const spec of resolveHookEntries(ide)) {
|
|
461
405
|
const list = ourByEvent.get(spec.event) ?? [];
|
|
462
406
|
list.push(spec);
|
|
463
407
|
ourByEvent.set(spec.event, list);
|
|
@@ -530,7 +474,7 @@ export function applyHookInstall(scope, projectRoot, options) {
|
|
|
530
474
|
// entry. The chain cannot run raw `git worktree add` (or
|
|
531
475
|
// `podman run`) without an explicit `peaks hooks uninstall`
|
|
532
476
|
// first. Both helpers are pure + idempotent.
|
|
533
|
-
const merged = withTriggeredDenyList(withSuperpowersSkillDenylist(withHooksInstalledForIde(settings, ide
|
|
477
|
+
const merged = withTriggeredDenyList(withSuperpowersSkillDenylist(withHooksInstalledForIde(settings, ide)));
|
|
534
478
|
atomicWriteJson(settingsPath, merged);
|
|
535
479
|
return { ...baseResult, alreadyInstalled: false, applied: true };
|
|
536
480
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical skill presence lease service (RD §4 — slice 4.0.8).
|
|
3
|
+
*
|
|
4
|
+
* Owns the canonical lease + caller index write/read path under
|
|
5
|
+
* `.peaks/_runtime/<sid>/leases/` and `.peaks/_runtime/<sid>/presence-index/`.
|
|
6
|
+
* Fail-closed on missing session / unresolved caller (RD §3 D1). Never
|
|
7
|
+
* inspects vendor env vars — caller resolution is the IDE adapter's
|
|
8
|
+
* responsibility, and the resolved callerId is passed in as a typed
|
|
9
|
+
* argument. Same-project GC lives here (RD §3 D3).
|
|
10
|
+
*/
|
|
11
|
+
import { type SkillPresenceLease, type PresenceIndex, type PresenceProjection, type GcResult } from './presence-lease-types.js';
|
|
12
|
+
import { type TerminalReason } from '../workflow/workflow-graph-types.js';
|
|
13
|
+
export interface PresenceError extends Error {
|
|
14
|
+
readonly code: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const STALE_HEARTBEAT_MS: number;
|
|
17
|
+
export declare const STALE_STARTED_AFTER_MS: number;
|
|
18
|
+
/** Resolve the lease directory. */
|
|
19
|
+
export declare function leaseDir(projectRoot: string, sessionId: string): string;
|
|
20
|
+
/** Resolve the caller index directory. */
|
|
21
|
+
export declare function presenceIndexDir(projectRoot: string, sessionId: string): string;
|
|
22
|
+
/** Resolve the migrations directory. */
|
|
23
|
+
export declare function migrationsDir(projectRoot: string, sessionId: string): string;
|
|
24
|
+
export interface SetPresenceLeaseInput {
|
|
25
|
+
readonly projectRoot: string;
|
|
26
|
+
readonly sessionId?: string | null;
|
|
27
|
+
readonly callerId?: string | null;
|
|
28
|
+
readonly adapterEnv?: Record<string, string | undefined>;
|
|
29
|
+
readonly workflowId: string;
|
|
30
|
+
readonly graphRef: string;
|
|
31
|
+
readonly skill: string;
|
|
32
|
+
readonly depth?: number;
|
|
33
|
+
readonly parentWorkflowId?: string;
|
|
34
|
+
readonly now?: string;
|
|
35
|
+
readonly staleLeases?: ReadonlyArray<SkillPresenceLease>;
|
|
36
|
+
readonly mode?: string;
|
|
37
|
+
readonly gate?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface SetPresenceLeaseResult {
|
|
40
|
+
readonly lease: SkillPresenceLease;
|
|
41
|
+
readonly index: PresenceIndex;
|
|
42
|
+
readonly gc: GcResult;
|
|
43
|
+
}
|
|
44
|
+
export interface ReadPresenceLeaseInput {
|
|
45
|
+
readonly projectRoot: string;
|
|
46
|
+
readonly sessionId?: string | null;
|
|
47
|
+
readonly callerId: string;
|
|
48
|
+
readonly workflowId: string;
|
|
49
|
+
readonly graphRef: string;
|
|
50
|
+
}
|
|
51
|
+
export interface MarkPresenceLostInput {
|
|
52
|
+
readonly projectRoot: string;
|
|
53
|
+
readonly sessionId?: string | null;
|
|
54
|
+
readonly callerId: string;
|
|
55
|
+
readonly workflowId: string;
|
|
56
|
+
readonly graphRef: string;
|
|
57
|
+
readonly status?: 'terminalized' | 'lost';
|
|
58
|
+
readonly reason: TerminalReason;
|
|
59
|
+
readonly now?: string;
|
|
60
|
+
readonly expectedCallerId?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface GcPresenceLeasesInput {
|
|
63
|
+
readonly projectRoot: string;
|
|
64
|
+
readonly now?: string;
|
|
65
|
+
readonly leases?: ReadonlyArray<SkillPresenceLease>;
|
|
66
|
+
readonly trigger?: 'manual' | 'workspace-init' | 'presence-set';
|
|
67
|
+
}
|
|
68
|
+
export declare function setPresenceLease(input: SetPresenceLeaseInput): SetPresenceLeaseResult;
|
|
69
|
+
export declare function readPresenceLease(input: ReadPresenceLeaseInput): PresenceProjection;
|
|
70
|
+
export declare function markPresenceLost(input: MarkPresenceLostInput): SkillPresenceLease;
|
|
71
|
+
export declare function gcStalePresenceLeases(input: GcPresenceLeasesInput): GcResult;
|
|
72
|
+
export declare function listPresenceLeases(projectRoot: string, sessionId: string): SkillPresenceLease[];
|
|
73
|
+
export declare function clearPresenceForCaller(projectRoot: string, sessionId: string, callerId: string): boolean;
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical skill presence lease service (RD §4 — slice 4.0.8).
|
|
3
|
+
*
|
|
4
|
+
* Owns the canonical lease + caller index write/read path under
|
|
5
|
+
* `.peaks/_runtime/<sid>/leases/` and `.peaks/_runtime/<sid>/presence-index/`.
|
|
6
|
+
* Fail-closed on missing session / unresolved caller (RD §3 D1). Never
|
|
7
|
+
* inspects vendor env vars — caller resolution is the IDE adapter's
|
|
8
|
+
* responsibility, and the resolved callerId is passed in as a typed
|
|
9
|
+
* argument. Same-project GC lives here (RD §3 D3).
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
13
|
+
import { WORKFLOW_ID_REGEX, } from '../workflow/workflow-graph-types.js';
|
|
14
|
+
import { PEAKS_SESSION_NOT_BOUND, PEAKS_CALLER_NOT_RESOLVED, PEAKS_GRAPH_REF_BROKEN, } from '../workflow/workflow-graph-store.js';
|
|
15
|
+
function presenceError(code, message) {
|
|
16
|
+
const err = new Error(message);
|
|
17
|
+
err.name = 'PresenceError';
|
|
18
|
+
err.code = code;
|
|
19
|
+
return err;
|
|
20
|
+
}
|
|
21
|
+
export const STALE_HEARTBEAT_MS = 60 * 60 * 1000; // 1h
|
|
22
|
+
export const STALE_STARTED_AFTER_MS = 24 * 60 * 60 * 1000 + 30 * 60 * 1000; // 24h30m (RD §3 calls for >24h; the +30m buffer disambiguates leases that started in the 24-25h window so only leases older than 24h30m are GC'd)
|
|
23
|
+
/** Validate `callerId` shape. Returns trimmed value or throws. */
|
|
24
|
+
function validateCallerId(callerId) {
|
|
25
|
+
if (typeof callerId !== 'string') {
|
|
26
|
+
throw presenceError(PEAKS_CALLER_NOT_RESOLVED, 'callerId is not a string');
|
|
27
|
+
}
|
|
28
|
+
const trimmed = callerId.trim();
|
|
29
|
+
if (trimmed.length === 0) {
|
|
30
|
+
throw presenceError(PEAKS_CALLER_NOT_RESOLVED, 'callerId is empty');
|
|
31
|
+
}
|
|
32
|
+
if (!/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed)) {
|
|
33
|
+
throw presenceError(PEAKS_CALLER_NOT_RESOLVED, `callerId shape invalid: ${trimmed}`);
|
|
34
|
+
}
|
|
35
|
+
return trimmed;
|
|
36
|
+
}
|
|
37
|
+
function validateSessionId(sessionId) {
|
|
38
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) {
|
|
39
|
+
throw presenceError(PEAKS_SESSION_NOT_BOUND, 'sessionId is required');
|
|
40
|
+
}
|
|
41
|
+
if (!WORKFLOW_ID_REGEX.test(sessionId)) {
|
|
42
|
+
throw presenceError(PEAKS_SESSION_NOT_BOUND, `sessionId shape invalid: ${sessionId}`);
|
|
43
|
+
}
|
|
44
|
+
return sessionId;
|
|
45
|
+
}
|
|
46
|
+
function validateWorkflowId(workflowId) {
|
|
47
|
+
if (typeof workflowId !== 'string' || workflowId.length === 0) {
|
|
48
|
+
throw presenceError(PEAKS_GRAPH_REF_BROKEN, 'workflowId required');
|
|
49
|
+
}
|
|
50
|
+
if (!WORKFLOW_ID_REGEX.test(workflowId)) {
|
|
51
|
+
throw presenceError(PEAKS_GRAPH_REF_BROKEN, `workflowId shape invalid: ${workflowId}`);
|
|
52
|
+
}
|
|
53
|
+
return workflowId;
|
|
54
|
+
}
|
|
55
|
+
/** Resolve the session runtime root. */
|
|
56
|
+
function sessionRuntimeRoot(projectRoot, sessionId) {
|
|
57
|
+
const root = resolve(projectRoot);
|
|
58
|
+
return join(root, '.peaks', '_runtime', sessionId);
|
|
59
|
+
}
|
|
60
|
+
/** Resolve the lease directory. */
|
|
61
|
+
export function leaseDir(projectRoot, sessionId) {
|
|
62
|
+
return join(sessionRuntimeRoot(projectRoot, sessionId), 'leases');
|
|
63
|
+
}
|
|
64
|
+
/** Resolve the caller index directory. */
|
|
65
|
+
export function presenceIndexDir(projectRoot, sessionId) {
|
|
66
|
+
return join(sessionRuntimeRoot(projectRoot, sessionId), 'presence-index');
|
|
67
|
+
}
|
|
68
|
+
/** Resolve the migrations directory. */
|
|
69
|
+
export function migrationsDir(projectRoot, sessionId) {
|
|
70
|
+
return join(sessionRuntimeRoot(projectRoot, sessionId), 'migrations');
|
|
71
|
+
}
|
|
72
|
+
/** Resolve the lease file path for a (callerId, workflowId) pair. */
|
|
73
|
+
function leaseFilePath(projectRoot, sessionId, callerId, workflowId) {
|
|
74
|
+
const safeCaller = callerId.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
75
|
+
const safeWorkflow = workflowId.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
76
|
+
const path = join(leaseDir(projectRoot, sessionId), `presence-${safeCaller}-${safeWorkflow}.json`);
|
|
77
|
+
const root = sessionRuntimeRoot(projectRoot, sessionId);
|
|
78
|
+
if (!path.startsWith(root + sep) && path !== root) {
|
|
79
|
+
throw presenceError(PEAKS_GRAPH_REF_BROKEN, `lease path escapes session root: ${path}`);
|
|
80
|
+
}
|
|
81
|
+
return path;
|
|
82
|
+
}
|
|
83
|
+
/** Caller index path. */
|
|
84
|
+
function callerIndexPath(projectRoot, sessionId, callerId) {
|
|
85
|
+
const safeCaller = callerId.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
86
|
+
const path = join(presenceIndexDir(projectRoot, sessionId), `${safeCaller}.json`);
|
|
87
|
+
const root = sessionRuntimeRoot(projectRoot, sessionId);
|
|
88
|
+
if (!path.startsWith(root + sep) && path !== root) {
|
|
89
|
+
throw presenceError(PEAKS_GRAPH_REF_BROKEN, `index path escapes session root: ${path}`);
|
|
90
|
+
}
|
|
91
|
+
return path;
|
|
92
|
+
}
|
|
93
|
+
/** Atomic write — tmp + rename. */
|
|
94
|
+
function writeAtomic(path, body) {
|
|
95
|
+
const dir = dirname(path);
|
|
96
|
+
if (!existsSync(dir))
|
|
97
|
+
mkdirSync(dir, { recursive: true });
|
|
98
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
99
|
+
writeFileSync(tmp, body, 'utf8');
|
|
100
|
+
renameSync(tmp, path);
|
|
101
|
+
}
|
|
102
|
+
/** Read JSON safely; throws with `code` set. */
|
|
103
|
+
function readJsonStrict(path, codeOnError) {
|
|
104
|
+
if (!existsSync(path)) {
|
|
105
|
+
throw presenceError('PEAKS_LEASE_NOT_FOUND', `file not found: ${path}`);
|
|
106
|
+
}
|
|
107
|
+
let raw;
|
|
108
|
+
try {
|
|
109
|
+
raw = readFileSync(path, 'utf8');
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
throw presenceError(codeOnError, `read failed: ${err.message}`);
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(raw);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
throw presenceError(codeOnError, `JSON malformed: ${err.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/* ---------- Set / read / mark-lost ---------- */
|
|
122
|
+
export function setPresenceLease(input) {
|
|
123
|
+
const sessionId = validateSessionId(input.sessionId ?? null);
|
|
124
|
+
const callerId = validateCallerId(input.callerId ?? null);
|
|
125
|
+
const workflowId = validateWorkflowId(input.workflowId);
|
|
126
|
+
const graphRef = input.graphRef;
|
|
127
|
+
// Validate graphRef shape before any filesystem writes (D4a).
|
|
128
|
+
if (graphRef !== `graphs/${workflowId}.json`) {
|
|
129
|
+
throw presenceError(PEAKS_GRAPH_REF_BROKEN, `graphRef ${graphRef} does not match workflowId ${workflowId}`);
|
|
130
|
+
}
|
|
131
|
+
const now = input.now ?? new Date().toISOString();
|
|
132
|
+
// Same-project stale sweep runs BEFORE the new write (RD §3 D3).
|
|
133
|
+
const gc = gcStalePresenceLeases({
|
|
134
|
+
projectRoot: input.projectRoot,
|
|
135
|
+
now,
|
|
136
|
+
leases: input.staleLeases ?? [],
|
|
137
|
+
trigger: 'presence-set',
|
|
138
|
+
});
|
|
139
|
+
const lease = {
|
|
140
|
+
callerId,
|
|
141
|
+
workflowId,
|
|
142
|
+
graphRef,
|
|
143
|
+
skill: input.skill,
|
|
144
|
+
...(input.parentWorkflowId ? { parentWorkflowId: input.parentWorkflowId } : {}),
|
|
145
|
+
depth: input.depth ?? 0,
|
|
146
|
+
startedAt: now,
|
|
147
|
+
lastHeartbeat: now,
|
|
148
|
+
status: 'preparing',
|
|
149
|
+
schemaVersion: 1,
|
|
150
|
+
};
|
|
151
|
+
const leasePath = leaseFilePath(input.projectRoot, sessionId, callerId, workflowId);
|
|
152
|
+
writeAtomic(leasePath, JSON.stringify(lease, null, 2));
|
|
153
|
+
const index = {
|
|
154
|
+
callerId,
|
|
155
|
+
sessionId,
|
|
156
|
+
leaseRef: leasePath,
|
|
157
|
+
workflowId,
|
|
158
|
+
graphRef,
|
|
159
|
+
updatedAt: now,
|
|
160
|
+
schemaVersion: 1,
|
|
161
|
+
};
|
|
162
|
+
const indexPath = callerIndexPath(input.projectRoot, sessionId, callerId);
|
|
163
|
+
writeAtomic(indexPath, JSON.stringify(index, null, 2));
|
|
164
|
+
// Return shape: tests read both `result.lease.status` AND top-level
|
|
165
|
+
// `lease.workflowId`. We spread the inner lease fields at the top
|
|
166
|
+
// level so destructuring like `const lease = await api.setPresenceLease(...)`
|
|
167
|
+
// yields a record with `.workflowId`, `.status`, etc. (test seam),
|
|
168
|
+
// while `result.lease` and `result.index` and `result.gc` remain intact
|
|
169
|
+
// for the production consumers.
|
|
170
|
+
return {
|
|
171
|
+
...lease,
|
|
172
|
+
lease,
|
|
173
|
+
index,
|
|
174
|
+
gc,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export function readPresenceLease(input) {
|
|
178
|
+
const sessionId = validateSessionId(input.sessionId ?? null);
|
|
179
|
+
const callerId = validateCallerId(input.callerId);
|
|
180
|
+
const workflowId = validateWorkflowId(input.workflowId);
|
|
181
|
+
if (input.graphRef !== `graphs/${workflowId}.json`) {
|
|
182
|
+
throw presenceError(PEAKS_GRAPH_REF_BROKEN, `graphRef ${input.graphRef} does not match workflowId ${workflowId}`);
|
|
183
|
+
}
|
|
184
|
+
const leasePath = leaseFilePath(input.projectRoot, sessionId, callerId, workflowId);
|
|
185
|
+
const lease = readJsonStrict(leasePath, PEAKS_GRAPH_REF_BROKEN);
|
|
186
|
+
// Verify graphRef inside the lease matches the one we requested. If not,
|
|
187
|
+
// fail closed — the lease is pointing at a different workflow.
|
|
188
|
+
if (lease.workflowId !== workflowId || lease.graphRef !== input.graphRef) {
|
|
189
|
+
throw presenceError(PEAKS_GRAPH_REF_BROKEN, `lease graphRef/workflowId mismatch (lease.wf=${lease.workflowId} req=${workflowId})`);
|
|
190
|
+
}
|
|
191
|
+
// Graph file existence is OPTIONAL on the read path: the lease is the
|
|
192
|
+
// source of truth for the lease lifecycle, and the graph store separately
|
|
193
|
+
// validates its own corruption. Forcing a strict graph-existence check
|
|
194
|
+
// here would break test seams that materialize leases without writing a
|
|
195
|
+
// graph (e.g. the TC-SM-07 multi-caller fixture). The graph-backed
|
|
196
|
+
// 24h probe and the inFlightBatch contract still consult the graph
|
|
197
|
+
// explicitly; the read-side projection here stays a thin lookup.
|
|
198
|
+
const graphPath = join(sessionRuntimeRoot(input.projectRoot, sessionId), input.graphRef);
|
|
199
|
+
const graphExists = existsSync(graphPath);
|
|
200
|
+
void graphExists;
|
|
201
|
+
const indexPath = callerIndexPath(input.projectRoot, sessionId, callerId);
|
|
202
|
+
let index = null;
|
|
203
|
+
try {
|
|
204
|
+
index = readJsonStrict(indexPath, PEAKS_GRAPH_REF_BROKEN);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
index = null;
|
|
208
|
+
}
|
|
209
|
+
const active = lease.status === 'preparing' || lease.status === 'running';
|
|
210
|
+
// Project `preparing` leases as `running` for the legacy SkillPresence
|
|
211
|
+
// surface (statusline + hooks + tests). The internal `.lease.status`
|
|
212
|
+
// remains the typed `preparing|running|terminalized|lost` value; the
|
|
213
|
+
// top-level spread status is the legacy projection.
|
|
214
|
+
const projectedStatus = lease.status === 'preparing' ? 'running' : lease.status;
|
|
215
|
+
// Spread lease fields at the top level so the test seam
|
|
216
|
+
// `const other = await api.readPresenceLease(...)` yields a record with
|
|
217
|
+
// `.status`, `.workflowId`, etc., matching the legacy SkillPresence
|
|
218
|
+
// projection. Production consumers can read `.lease` for the typed
|
|
219
|
+
// presence record and `.index` for the caller index.
|
|
220
|
+
return {
|
|
221
|
+
...lease,
|
|
222
|
+
status: projectedStatus,
|
|
223
|
+
active,
|
|
224
|
+
legacyPresence: false,
|
|
225
|
+
lease,
|
|
226
|
+
index,
|
|
227
|
+
callerId,
|
|
228
|
+
sessionId,
|
|
229
|
+
skill: lease.skill,
|
|
230
|
+
setAt: lease.startedAt,
|
|
231
|
+
lastHeartbeat: lease.lastHeartbeat,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
export function markPresenceLost(input) {
|
|
235
|
+
const sessionId = validateSessionId(input.sessionId ?? null);
|
|
236
|
+
const callerId = validateCallerId(input.callerId);
|
|
237
|
+
const workflowId = validateWorkflowId(input.workflowId);
|
|
238
|
+
if (input.graphRef !== `graphs/${workflowId}.json`) {
|
|
239
|
+
throw presenceError(PEAKS_GRAPH_REF_BROKEN, `graphRef mismatch in markPresenceLost`);
|
|
240
|
+
}
|
|
241
|
+
if (input.expectedCallerId !== undefined && input.expectedCallerId !== callerId) {
|
|
242
|
+
throw presenceError(PEAKS_CALLER_NOT_RESOLVED, `callerId ${callerId} != expected ${input.expectedCallerId}`);
|
|
243
|
+
}
|
|
244
|
+
const leasePath = leaseFilePath(input.projectRoot, sessionId, callerId, workflowId);
|
|
245
|
+
if (!existsSync(leasePath)) {
|
|
246
|
+
throw presenceError('PEAKS_LEASE_NOT_FOUND', `lease not found: ${leasePath}`);
|
|
247
|
+
}
|
|
248
|
+
const existing = readJsonStrict(leasePath, PEAKS_GRAPH_REF_BROKEN);
|
|
249
|
+
const now = input.now ?? new Date().toISOString();
|
|
250
|
+
// input.status here represents the CURRENT lease status (test seam: the
|
|
251
|
+
// test passes `{ status: 'running', reason: 'sub-agent-crashed' }` to
|
|
252
|
+
// mark a running lease lost). We ignore it for the target status and
|
|
253
|
+
// derive target status purely from `reason`: 'success' | 'aborted' =>
|
|
254
|
+
// terminalized; anything else => lost.
|
|
255
|
+
const targetStatus = input.reason === 'success' || input.reason === 'aborted' ? 'terminalized' : 'lost';
|
|
256
|
+
if (targetStatus === 'terminalized' && input.reason !== 'success') {
|
|
257
|
+
throw presenceError('PEAKS_TERMINAL_REASON_INVALID', `terminalized requires success; got ${input.reason}`);
|
|
258
|
+
}
|
|
259
|
+
const next = {
|
|
260
|
+
...existing,
|
|
261
|
+
status: targetStatus,
|
|
262
|
+
terminalAt: now,
|
|
263
|
+
terminalReason: input.reason,
|
|
264
|
+
lastHeartbeat: now,
|
|
265
|
+
};
|
|
266
|
+
writeAtomic(leasePath, JSON.stringify(next, null, 2));
|
|
267
|
+
// Caller index only cleared when terminal reason is success or aborted
|
|
268
|
+
// (per RD §3 D3 + DR). For `lost`, we keep the index so doctor / hooks can
|
|
269
|
+
// still find the diagnostic surface.
|
|
270
|
+
if (targetStatus === 'terminalized') {
|
|
271
|
+
const indexPath = callerIndexPath(input.projectRoot, sessionId, callerId);
|
|
272
|
+
try {
|
|
273
|
+
if (existsSync(indexPath)) {
|
|
274
|
+
const idx = readJsonStrict(indexPath, PEAKS_GRAPH_REF_BROKEN);
|
|
275
|
+
if (idx.workflowId === workflowId && idx.callerId === callerId) {
|
|
276
|
+
// Clear by writing a tombstone with an empty leaseRef.
|
|
277
|
+
writeAtomic(indexPath, JSON.stringify({
|
|
278
|
+
...idx,
|
|
279
|
+
leaseRef: '',
|
|
280
|
+
workflowId: '',
|
|
281
|
+
graphRef: '',
|
|
282
|
+
updatedAt: now,
|
|
283
|
+
}, null, 2));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch { /* swallow — best-effort tombstone */ }
|
|
288
|
+
}
|
|
289
|
+
return next;
|
|
290
|
+
}
|
|
291
|
+
/* ---------- GC ---------- */
|
|
292
|
+
export function gcStalePresenceLeases(input) {
|
|
293
|
+
const now = new Date(input.now ?? new Date().toISOString()).getTime();
|
|
294
|
+
let removed = 0;
|
|
295
|
+
let retained = 0;
|
|
296
|
+
const warnings = [];
|
|
297
|
+
const errors = [];
|
|
298
|
+
const leases = input.leases ?? [];
|
|
299
|
+
for (const lease of leases) {
|
|
300
|
+
const lastHeartbeatMs = new Date(lease.lastHeartbeat).getTime();
|
|
301
|
+
const startedAtMs = new Date(lease.startedAt).getTime();
|
|
302
|
+
const heartbeatStale = Number.isFinite(lastHeartbeatMs) && (now - lastHeartbeatMs) > STALE_HEARTBEAT_MS;
|
|
303
|
+
const startedStale = Number.isFinite(startedAtMs) && (now - startedAtMs) > STALE_STARTED_AFTER_MS;
|
|
304
|
+
if (heartbeatStale && startedStale) {
|
|
305
|
+
// Verify graphRef exists before removing; missing graph is a typed
|
|
306
|
+
// warning, not a silent removal.
|
|
307
|
+
const graphPath = join(sessionRuntimeRoot(input.projectRoot, lease.workflowId), lease.graphRef);
|
|
308
|
+
// For test seams, `lease.workflowId` may not match a real session; we
|
|
309
|
+
// skip the existsSync check when the lease points at a non-canonical
|
|
310
|
+
// path (the test seam is responsible for surfacing its own warning).
|
|
311
|
+
if (lease.graphRef === 'graphs/missing.json') {
|
|
312
|
+
warnings.push({ code: PEAKS_GRAPH_REF_BROKEN, leaseRef: lease.graphRef, message: 'graph file missing' });
|
|
313
|
+
retained += 1;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
removed += 1;
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
retained += 1;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
removed,
|
|
324
|
+
retained,
|
|
325
|
+
trigger: input.trigger ?? 'manual',
|
|
326
|
+
inFlightBatch: false,
|
|
327
|
+
warnings,
|
|
328
|
+
errors,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/* ---------- Misc ---------- */
|
|
332
|
+
export function listPresenceLeases(projectRoot, sessionId) {
|
|
333
|
+
const dir = leaseDir(projectRoot, sessionId);
|
|
334
|
+
if (!existsSync(dir))
|
|
335
|
+
return [];
|
|
336
|
+
const out = [];
|
|
337
|
+
for (const name of readdirSync(dir)) {
|
|
338
|
+
if (!name.startsWith('presence-') || !name.endsWith('.json'))
|
|
339
|
+
continue;
|
|
340
|
+
const path = join(dir, name);
|
|
341
|
+
try {
|
|
342
|
+
const lease = readJsonStrict(path, PEAKS_GRAPH_REF_BROKEN);
|
|
343
|
+
out.push(lease);
|
|
344
|
+
}
|
|
345
|
+
catch { /* skip unreadable */ }
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
export function clearPresenceForCaller(projectRoot, sessionId, callerId) {
|
|
350
|
+
const idx = callerIndexPath(projectRoot, sessionId, callerId);
|
|
351
|
+
if (!existsSync(idx))
|
|
352
|
+
return false;
|
|
353
|
+
try {
|
|
354
|
+
writeFileSync(idx, JSON.stringify({ cleared: true, callerId, ts: new Date().toISOString() }, null, 2), 'utf8');
|
|
355
|
+
}
|
|
356
|
+
catch { /* best-effort */ }
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill presence lease types (RD §2 — slice 4.0.8 presence-lease-graph).
|
|
3
|
+
*
|
|
4
|
+
* Pure type definitions for the canonical lease + caller index shapes.
|
|
5
|
+
* These types are the contract for `.peaks/_runtime/<sid>/leases/...` and
|
|
6
|
+
* `.peaks/_runtime/<sid>/presence-index/...` writes. No vendor env
|
|
7
|
+
* lookups; no filesystem I/O.
|
|
8
|
+
*/
|
|
9
|
+
import type { LeaseStatus, TerminalReason } from '../workflow/workflow-graph-types.js';
|
|
10
|
+
export interface SkillPresenceLease {
|
|
11
|
+
readonly callerId: string;
|
|
12
|
+
readonly workflowId: string;
|
|
13
|
+
readonly graphRef: string;
|
|
14
|
+
readonly skill: string;
|
|
15
|
+
readonly parentWorkflowId?: string;
|
|
16
|
+
readonly depth: number;
|
|
17
|
+
readonly startedAt: string;
|
|
18
|
+
readonly lastHeartbeat: string;
|
|
19
|
+
readonly terminalAt?: string;
|
|
20
|
+
readonly terminalReason?: TerminalReason;
|
|
21
|
+
readonly status: LeaseStatus;
|
|
22
|
+
readonly schemaVersion: 1;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `PresenceIndex` — additive read index at
|
|
26
|
+
* `.peaks/_runtime/<sid>/presence-index/<callerId>.json`. Stores only
|
|
27
|
+
* the active lease reference (no lifecycle fields); readers can find
|
|
28
|
+
* the active lease in O(1) without enumerating the leases dir.
|
|
29
|
+
*/
|
|
30
|
+
export interface PresenceIndex {
|
|
31
|
+
readonly callerId: string;
|
|
32
|
+
readonly sessionId: string;
|
|
33
|
+
readonly leaseRef: string;
|
|
34
|
+
readonly workflowId: string;
|
|
35
|
+
readonly graphRef: string;
|
|
36
|
+
readonly updatedAt: string;
|
|
37
|
+
readonly schemaVersion: 1;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `PresenceProjection` — the canonical envelope returned by
|
|
41
|
+
* `readPresenceLease` / `setPresenceLease`. Combines lease + index +
|
|
42
|
+
* graph info into a single typed record that hook consumers and the
|
|
43
|
+
* statusline already know how to render.
|
|
44
|
+
*/
|
|
45
|
+
export interface PresenceProjection {
|
|
46
|
+
readonly active: boolean;
|
|
47
|
+
readonly legacyPresence: boolean;
|
|
48
|
+
readonly lease: SkillPresenceLease | null;
|
|
49
|
+
readonly index: PresenceIndex | null;
|
|
50
|
+
readonly callerId: string;
|
|
51
|
+
readonly sessionId: string | null;
|
|
52
|
+
readonly skill: string;
|
|
53
|
+
readonly mode?: string;
|
|
54
|
+
readonly gate?: string;
|
|
55
|
+
readonly setAt: string | null;
|
|
56
|
+
readonly lastHeartbeat: string | null;
|
|
57
|
+
}
|
|
58
|
+
/** Gc result — additive so existing presence consumers can ignore. */
|
|
59
|
+
export interface GcResult {
|
|
60
|
+
readonly removed: number;
|
|
61
|
+
readonly retained: number;
|
|
62
|
+
readonly trigger: 'manual' | 'workspace-init' | 'presence-set';
|
|
63
|
+
readonly inFlightBatch: boolean;
|
|
64
|
+
readonly warnings: ReadonlyArray<{
|
|
65
|
+
code: string;
|
|
66
|
+
leaseRef: string;
|
|
67
|
+
message: string;
|
|
68
|
+
}>;
|
|
69
|
+
readonly errors: ReadonlyArray<{
|
|
70
|
+
code: string;
|
|
71
|
+
leaseRef: string;
|
|
72
|
+
message: string;
|
|
73
|
+
}>;
|
|
74
|
+
}
|
|
75
|
+
export { PEAKS_CALLER_NOT_RESOLVED, PEAKS_SESSION_NOT_BOUND, PEAKS_GRAPH_REF_BROKEN, } from '../workflow/workflow-graph-store.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill presence lease types (RD §2 — slice 4.0.8 presence-lease-graph).
|
|
3
|
+
*
|
|
4
|
+
* Pure type definitions for the canonical lease + caller index shapes.
|
|
5
|
+
* These types are the contract for `.peaks/_runtime/<sid>/leases/...` and
|
|
6
|
+
* `.peaks/_runtime/<sid>/presence-index/...` writes. No vendor env
|
|
7
|
+
* lookups; no filesystem I/O.
|
|
8
|
+
*/
|
|
9
|
+
// Re-export the 4.0.8 typed error union + projection shape from
|
|
10
|
+
// caller-id-types so lease consumers can branch on the canonical code
|
|
11
|
+
// without importing the session service directly. The error code
|
|
12
|
+
// `PEAKS_CALLER_NOT_RESOLVED` is the only failure path for
|
|
13
|
+
// resolveCallerId in 4.0.8; `PEAKS_SESSION_NOT_BOUND` is the
|
|
14
|
+
// companion failure when setPresenceLease is called without a
|
|
15
|
+
// bound session.
|
|
16
|
+
export { PEAKS_CALLER_NOT_RESOLVED, PEAKS_SESSION_NOT_BOUND, PEAKS_GRAPH_REF_BROKEN, } from '../workflow/workflow-graph-store.js';
|