@wrongstack/core 0.303.0 → 0.305.0
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/dist/coordination/agents/index.js +530 -130
- package/dist/coordination/agents/project-agent-consolidation.d.ts +5 -0
- package/dist/coordination/agents/project-agent-directive-outcome.d.ts +57 -0
- package/dist/coordination/agents/project-agent-identity.d.ts +26 -12
- package/dist/coordination/agents/project-agent-learning-policy.d.ts +22 -1
- package/dist/coordination/agents/project-agent-learning-structured.d.ts +46 -1
- package/dist/coordination/agents/project-agent-quarantine.d.ts +63 -0
- package/dist/coordination/agents/project-agent-skill-layer.d.ts +55 -10
- package/dist/coordination/agents/types.d.ts +10 -2
- package/dist/coordination/director-prompts.d.ts +19 -6
- package/dist/coordination/director-tools.d.ts +2 -2
- package/dist/coordination/fleet.d.ts +0 -6
- package/dist/coordination/index.d.ts +1 -1
- package/dist/coordination/index.js +1579 -955
- package/dist/core/agent-types.d.ts +4 -2
- package/dist/core/agent.d.ts +1 -0
- package/dist/core/context.d.ts +15 -0
- package/dist/core/conversation-state.d.ts +14 -0
- package/dist/core/fallback-profile-manager.d.ts +70 -2
- package/dist/core/index.js +308 -108
- package/dist/core/system-prompt-blocks.d.ts +1 -1
- package/dist/core/system-prompt-builder.d.ts +13 -1
- package/dist/core/system-prompt-glossary.d.ts +73 -0
- package/dist/core/system-prompt-memory-skills.d.ts +2 -2
- package/dist/defaults/index.js +910 -693
- package/dist/execution/council-orchestrator.d.ts +3 -13
- package/dist/execution/index.js +211 -75
- package/dist/execution/one-shot-llm.d.ts +5 -0
- package/dist/hq/index.js +17 -7
- package/dist/hq/protocol/kanban.d.ts +21 -0
- package/dist/hq/protocol.js +5 -1
- package/dist/hq/redaction.d.ts +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3482 -2538
- package/dist/infrastructure/index.js +247 -122
- package/dist/plugin/index.js +101 -3
- package/dist/registry/index.js +11 -0
- package/dist/registry/tool-registry.d.ts +8 -0
- package/dist/replay/hash.d.ts +9 -0
- package/dist/replay/index.js +14 -4
- package/dist/replay/replay-provider-runner.d.ts +31 -1
- package/dist/security/index.js +25 -20
- package/dist/security/secret-vault.d.ts +2 -0
- package/dist/session-catalog/index.js +62 -8
- package/dist/session-catalog/project-server.js +73 -13
- package/dist/session-catalog/protocol.d.ts +11 -4
- package/dist/session-catalog/store.d.ts +2 -2
- package/dist/storage/index.js +224 -67
- package/dist/storage/memory-consolidator.d.ts +4 -2
- package/dist/storage/session-resume-validation.d.ts +24 -0
- package/dist/storage/session-store/directory-scan.d.ts +5 -1
- package/dist/storage/session-store/fork-session.d.ts +13 -1
- package/dist/storage/session-store/load-cache.d.ts +11 -0
- package/dist/storage/session-store/prune-helpers.d.ts +5 -0
- package/dist/storage/session-store.d.ts +18 -0
- package/dist/tools/index.js +174 -74
- package/dist/types/config/mcp-features.d.ts +31 -1
- package/dist/types/config/root.d.ts +12 -0
- package/dist/types/config/tools.d.ts +22 -0
- package/dist/types/default-config.d.ts +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/session.d.ts +9 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +214 -76
- package/dist/utils/project-state-guard.d.ts +21 -0
- package/dist/utils/session-scoped-path.d.ts +17 -0
- package/dist/utils/todos-format.d.ts +20 -0
- package/instructions/leader-after-task.md +3 -4
- package/instructions/system-lite.md +10 -13
- package/instructions/system-pro.md +18 -25
- package/instructions/system.md +18 -23
- package/package.json +3 -3
- package/skills/wrongstack-kanban/SKILL.md +95 -124
package/dist/plugin/index.js
CHANGED
|
@@ -2443,12 +2443,97 @@ var FallbackProfileManager = class {
|
|
|
2443
2443
|
return this.resolveRefs([ref], exclude);
|
|
2444
2444
|
}
|
|
2445
2445
|
/**
|
|
2446
|
-
* Resolve every usable configured target as
|
|
2446
|
+
* Resolve every usable configured target as a bounded last-resort chain.
|
|
2447
2447
|
* Normal smart defaults stay bounded; callers append this only after the
|
|
2448
|
-
* preferred chain and only when automatic fallback is enabled.
|
|
2448
|
+
* preferred chain and only when automatic fallback is enabled. The cap
|
|
2449
|
+
* ({@link MAX_LAST_RESORT_CANDIDATES}) prevents a config with many providers
|
|
2450
|
+
* from producing a degenerate chain of doomed requests during a systemic
|
|
2451
|
+
* outage — by this point the smart default, bridge, and default profile
|
|
2452
|
+
* have already failed.
|
|
2449
2453
|
*/
|
|
2450
2454
|
resolveAllConfigured(exclude) {
|
|
2451
|
-
return this.smartDefault(exclude,
|
|
2455
|
+
return this.smartDefault(exclude, this.lastResortCap());
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Effective cap for the last-resort append. Reads the user-configurable
|
|
2459
|
+
* {@link Config.fallbackMaxLastResortCandidates} when set and valid;
|
|
2460
|
+
* otherwise falls back to the compiled-in default
|
|
2461
|
+
* {@link MAX_LAST_RESORT_CANDIDATES}.
|
|
2462
|
+
*/
|
|
2463
|
+
lastResortCap() {
|
|
2464
|
+
const configured = this.config.fallbackMaxLastResortCandidates;
|
|
2465
|
+
if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0) {
|
|
2466
|
+
return Math.floor(configured);
|
|
2467
|
+
}
|
|
2468
|
+
return MAX_LAST_RESORT_CANDIDATES;
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* Build the complete fallback candidate chain shared by the agent-loop
|
|
2472
|
+
* extension and the one-shot orchestrator. Centralizes the bridge → primary →
|
|
2473
|
+
* selected → default-profile → all-configured ladder and the fromExplicitSource
|
|
2474
|
+
* gate so both consumers produce identical ordering and depth semantics.
|
|
2475
|
+
*
|
|
2476
|
+
* Layering (each step deduped against all prior):
|
|
2477
|
+
* 1. Bridge (emergency continuity route).
|
|
2478
|
+
* 2. Configured primary, when the live context drifted from it.
|
|
2479
|
+
* 3. The selected chain (explicit refs → named profile → smart default),
|
|
2480
|
+
* via {@link resolveEffective}.
|
|
2481
|
+
* 4. The "default" profile — extra depth, ONLY when the chain was auto-derived.
|
|
2482
|
+
* 5. Every other configured provider — last resort, ONLY when the chain was
|
|
2483
|
+
* auto-derived AND `effectiveFallbackAuto` is true.
|
|
2484
|
+
*
|
|
2485
|
+
* Returns the empty chain when `closedWorld` is true and no explicit
|
|
2486
|
+
* refs/profile resolved — a model allowlist never leaks to unlisted models.
|
|
2487
|
+
*
|
|
2488
|
+
* @internal caller-aware options (primary, closedWorld) are accepted because
|
|
2489
|
+
* the agent loop has context the manager does not own; the resolution
|
|
2490
|
+
* pipeline itself is identical for both callers.
|
|
2491
|
+
*/
|
|
2492
|
+
resolveCandidates(current, opts = {}) {
|
|
2493
|
+
const configuredPrimary = opts.primary ?? {
|
|
2494
|
+
providerId: this.config.provider,
|
|
2495
|
+
model: this.config.model
|
|
2496
|
+
};
|
|
2497
|
+
const configFallbackAuto = this.config.fallbackAuto;
|
|
2498
|
+
const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
|
|
2499
|
+
const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
|
|
2500
|
+
const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
|
|
2501
|
+
const profileUsable = opts.fallbackProfile !== void 0 && this.hasProfile(opts.fallbackProfile) && this.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
|
|
2502
|
+
const fromExplicitSource = explicitUsable || profileUsable;
|
|
2503
|
+
const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? this.resolve(opts.fallbackProfile, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
|
|
2504
|
+
fallbackModels: explicitRefs,
|
|
2505
|
+
fallbackProfile: opts.fallbackProfile,
|
|
2506
|
+
fallbackAuto: effectiveFallbackAuto,
|
|
2507
|
+
exclude: current
|
|
2508
|
+
});
|
|
2509
|
+
const candidates = [];
|
|
2510
|
+
if (opts.closedWorld) {
|
|
2511
|
+
candidates.push(...selectedChain);
|
|
2512
|
+
} else {
|
|
2513
|
+
candidates.push(...this.resolveBridge(current));
|
|
2514
|
+
if (!(configuredPrimary.providerId === current.providerId && configuredPrimary.model === current.model)) {
|
|
2515
|
+
candidates.push({
|
|
2516
|
+
providerId: configuredPrimary.providerId,
|
|
2517
|
+
model: configuredPrimary.model,
|
|
2518
|
+
providerSwitched: configuredPrimary.providerId !== current.providerId
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
candidates.push(...selectedChain);
|
|
2522
|
+
if (!fromExplicitSource && effectiveFallbackAuto && opts.fallbackProfile !== "default") {
|
|
2523
|
+
candidates.push(...this.resolve("default", { exclude: current }));
|
|
2524
|
+
}
|
|
2525
|
+
if (!fromExplicitSource && effectiveFallbackAuto) {
|
|
2526
|
+
const cap = this.lastResortCap();
|
|
2527
|
+
if (cap > 0) {
|
|
2528
|
+
const usedKeys = new Set(
|
|
2529
|
+
candidates.map((c) => `${c.providerId}/${c.model}`)
|
|
2530
|
+
);
|
|
2531
|
+
const lastResort = this.smartDefault(current, Number.POSITIVE_INFINITY).filter((c) => !usedKeys.has(`${c.providerId}/${c.model}`)).slice(0, cap);
|
|
2532
|
+
candidates.push(...lastResort);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
return dedupeChain(candidates, current);
|
|
2452
2537
|
}
|
|
2453
2538
|
// ── Provider availability (read-only) ──────────────────────────────────
|
|
2454
2539
|
checkProvider(providerId) {
|
|
@@ -2572,7 +2657,20 @@ var FallbackProfileManager = class {
|
|
|
2572
2657
|
);
|
|
2573
2658
|
}
|
|
2574
2659
|
};
|
|
2660
|
+
var MAX_LAST_RESORT_CANDIDATES = 12;
|
|
2575
2661
|
var FREEZER_EMPTY = Object.freeze([]);
|
|
2662
|
+
function dedupeChain(entries, current) {
|
|
2663
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2664
|
+
const currentKey = `${current.providerId}/${current.model}`;
|
|
2665
|
+
return Object.freeze(
|
|
2666
|
+
entries.filter((entry) => {
|
|
2667
|
+
const key = `${entry.providerId}/${entry.model}`;
|
|
2668
|
+
if (key === currentKey || seen.has(key)) return false;
|
|
2669
|
+
seen.add(key);
|
|
2670
|
+
return true;
|
|
2671
|
+
})
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2576
2674
|
|
|
2577
2675
|
// src/plugins/auto-review-plugin.ts
|
|
2578
2676
|
init_error();
|
package/dist/registry/index.js
CHANGED
|
@@ -759,6 +759,17 @@ var ToolRegistry = class _ToolRegistry {
|
|
|
759
759
|
this._providerListSnapshotVersion = this._version;
|
|
760
760
|
return arr;
|
|
761
761
|
}
|
|
762
|
+
/**
|
|
763
|
+
* Whether an enabled catalog tool is part of the direct provider surface.
|
|
764
|
+
*
|
|
765
|
+
* This is intentionally different from `get()`/`list()`: a tool can remain
|
|
766
|
+
* executable through a lazy gateway while its schema is omitted from the
|
|
767
|
+
* next provider request. Disabled and unknown tools always return false.
|
|
768
|
+
*/
|
|
769
|
+
isExposedToProvider(name) {
|
|
770
|
+
if (this._disabled.has(name) || !this.tools.has(name)) return false;
|
|
771
|
+
return this._providerToolNames?.has(name) ?? true;
|
|
772
|
+
}
|
|
762
773
|
/**
|
|
763
774
|
* Group tools by their `category` field. Tools without a category
|
|
764
775
|
* are placed under the key `""` (empty string). Returns a Map of
|
|
@@ -149,6 +149,14 @@ export declare class ToolRegistry {
|
|
|
149
149
|
exposeToProvider(names: string | readonly string[]): void;
|
|
150
150
|
/** Tools serialized into provider requests and described by the system prompt. */
|
|
151
151
|
listForProvider(): Tool[];
|
|
152
|
+
/**
|
|
153
|
+
* Whether an enabled catalog tool is part of the direct provider surface.
|
|
154
|
+
*
|
|
155
|
+
* This is intentionally different from `get()`/`list()`: a tool can remain
|
|
156
|
+
* executable through a lazy gateway while its schema is omitted from the
|
|
157
|
+
* next provider request. Disabled and unknown tools always return false.
|
|
158
|
+
*/
|
|
159
|
+
isExposedToProvider(name: string): boolean;
|
|
152
160
|
/**
|
|
153
161
|
* Group tools by their `category` field. Tools without a category
|
|
154
162
|
* are placed under the key `""` (empty string). Returns a Map of
|
package/dist/replay/hash.d.ts
CHANGED
|
@@ -17,6 +17,15 @@ import type { Request } from '../types/provider.js';
|
|
|
17
17
|
* sampling knobs (`temperature`, `topP`, `stopSequences`,
|
|
18
18
|
* `toolChoice`). Anything else on the `Request` (metadata,
|
|
19
19
|
* future extensions) is ignored so replay stays forward-compat.
|
|
20
|
+
* - The same rule applies *inside* each message. `Request.messages`
|
|
21
|
+
* is the live `ctx.messages` array, whose entries carry local
|
|
22
|
+
* bookkeeping the provider adapters explicitly drop: `ts` (wall
|
|
23
|
+
* clock at the moment the turn was created), `_estTokens` (a
|
|
24
|
+
* mutation-time cache) and `origin`. Hashing those made every
|
|
25
|
+
* hash a function of when the run happened, so a recorded
|
|
26
|
+
* response could never be found again and `mode: 'replay'` threw
|
|
27
|
+
* on the first call it was asked to serve. See
|
|
28
|
+
* {@link semanticMessage}.
|
|
20
29
|
* - We serialize to JSON. The `ContentBlock` and `Message` shapes
|
|
21
30
|
* are pure data; this works as long as no `undefined` values
|
|
22
31
|
* sneak in (those get dropped by `JSON.stringify`, which is
|
package/dist/replay/index.js
CHANGED
|
@@ -15,11 +15,15 @@ function sortKeys(value) {
|
|
|
15
15
|
}
|
|
16
16
|
return value;
|
|
17
17
|
}
|
|
18
|
+
function semanticMessage(message) {
|
|
19
|
+
const { ts: _ts, _estTokens: _estimate, origin: _origin, ...semantic } = message;
|
|
20
|
+
return semantic;
|
|
21
|
+
}
|
|
18
22
|
function hashRequest(request) {
|
|
19
23
|
const payload = {
|
|
20
24
|
model: request.model,
|
|
21
25
|
system: request.system,
|
|
22
|
-
messages: request.messages,
|
|
26
|
+
messages: request.messages.map(semanticMessage),
|
|
23
27
|
tools: request.tools,
|
|
24
28
|
maxTokens: request.maxTokens,
|
|
25
29
|
temperature: request.temperature,
|
|
@@ -40,16 +44,22 @@ var ReplayProviderRunner = class {
|
|
|
40
44
|
}
|
|
41
45
|
inner;
|
|
42
46
|
opts;
|
|
47
|
+
/** Resolve the target session for this call. See {@link ReplayProviderRunnerOptions.sessionId}. */
|
|
48
|
+
resolveSessionId() {
|
|
49
|
+
const { sessionId } = this.opts;
|
|
50
|
+
return typeof sessionId === "function" ? sessionId() : sessionId;
|
|
51
|
+
}
|
|
43
52
|
async run(runOpts) {
|
|
44
53
|
const hash = hashRequest(runOpts.request);
|
|
45
|
-
const
|
|
54
|
+
const sessionId = this.resolveSessionId();
|
|
55
|
+
const cached = await this.opts.log.lookup(sessionId, hash);
|
|
46
56
|
if (this.opts.mode === "replay") {
|
|
47
57
|
if (!cached) {
|
|
48
58
|
this.opts.logger?.warn?.(
|
|
49
59
|
`replay: no recorded response for hash ${hash} (model ${runOpts.request.model})`
|
|
50
60
|
);
|
|
51
61
|
throw new Error(
|
|
52
|
-
`ReplayProviderRunner: no recorded response for hash ${hash} in session ${
|
|
62
|
+
`ReplayProviderRunner: no recorded response for hash ${hash} in session ${sessionId}. Either the request changed since recording, or this session has no replay log.`
|
|
53
63
|
);
|
|
54
64
|
}
|
|
55
65
|
this.opts.logger?.debug?.(
|
|
@@ -65,7 +75,7 @@ var ReplayProviderRunner = class {
|
|
|
65
75
|
}
|
|
66
76
|
const response = await this.inner.run(runOpts);
|
|
67
77
|
await this.opts.log.record({
|
|
68
|
-
sessionId
|
|
78
|
+
sessionId,
|
|
69
79
|
request: runOpts.request,
|
|
70
80
|
response
|
|
71
81
|
});
|
|
@@ -26,11 +26,39 @@ import type { Response } from '../types/provider.js';
|
|
|
26
26
|
* a particular request is seen, it's recorded; the next time
|
|
27
27
|
* the same request is made, the recorded response is served.
|
|
28
28
|
* This is the cheapest path to a deterministic test harness.
|
|
29
|
+
*
|
|
30
|
+
* ## What a hit requires
|
|
31
|
+
*
|
|
32
|
+
* The lookup key is `hashRequest`, which covers everything the provider is
|
|
33
|
+
* actually sent — including `system`. The agent loop appends volatile blocks
|
|
34
|
+
* to the system prompt on every request (the continuity ledger, the live
|
|
35
|
+
* next-steps gate, memory evidence; see `agent-response.ts`), and those blocks
|
|
36
|
+
* describe the state of *this* run. A re-run whose ledger or retrieved memory
|
|
37
|
+
* differs therefore produces a different request, and correctly misses: the
|
|
38
|
+
* recorded response answered a different prompt.
|
|
39
|
+
*
|
|
40
|
+
* So `mode: 'replay'` reproduces a run faithfully only when the loop is driven
|
|
41
|
+
* back through the same states — which is what a test harness does, and what a
|
|
42
|
+
* fresh interactive session does not. In an interactive session, prefer
|
|
43
|
+
* `'record'` (build the log) and use `'replay'` from a harness that pins the
|
|
44
|
+
* volatile inputs. `hashRequest` itself is stable across time: local message
|
|
45
|
+
* bookkeeping (`ts`, `_estTokens`, `origin`) is stripped before hashing.
|
|
29
46
|
*/
|
|
30
47
|
export type ReplayMode = 'record' | 'replay' | 'auto';
|
|
31
48
|
export interface ReplayProviderRunnerOptions {
|
|
32
49
|
log: ReplayLogStore;
|
|
33
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Which session's log to read and write.
|
|
52
|
+
*
|
|
53
|
+
* Accepts a resolver because recording is wired before the session exists:
|
|
54
|
+
* the boot path binds this runner while the session writer is still being
|
|
55
|
+
* created, and the user may then swap sessions (`/resume`, new session)
|
|
56
|
+
* without the runner being rebuilt. A frozen string there meant the log was
|
|
57
|
+
* filed under a synthetic id with no session behind it. A resolver keeps the
|
|
58
|
+
* log next to the transcript it belongs to, for the session that is actually
|
|
59
|
+
* live at the time of the call.
|
|
60
|
+
*/
|
|
61
|
+
sessionId: string | (() => string);
|
|
34
62
|
mode: ReplayMode;
|
|
35
63
|
/**
|
|
36
64
|
* Optional logger — receives a debug line on every replay hit
|
|
@@ -48,6 +76,8 @@ export declare class ReplayProviderRunner implements ProviderRunner {
|
|
|
48
76
|
private readonly inner;
|
|
49
77
|
private readonly opts;
|
|
50
78
|
constructor(inner: ProviderRunner, opts: ReplayProviderRunnerOptions);
|
|
79
|
+
/** Resolve the target session for this call. See {@link ReplayProviderRunnerOptions.sessionId}. */
|
|
80
|
+
private resolveSessionId;
|
|
51
81
|
run(runOpts: RunProviderOptions): Promise<Response>;
|
|
52
82
|
}
|
|
53
83
|
//# sourceMappingURL=replay-provider-runner.d.ts.map
|
package/dist/security/index.js
CHANGED
|
@@ -1349,7 +1349,7 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
|
|
|
1349
1349
|
} : { decision: "allow" };
|
|
1350
1350
|
}
|
|
1351
1351
|
const task = identity.taskId ? board.tasks.find((candidate) => candidate.id === identity.taskId) : void 0;
|
|
1352
|
-
if (governanceRequired) {
|
|
1352
|
+
if (governanceRequired && board.lifecycle?.mode === "managed") {
|
|
1353
1353
|
if (!identity.taskId) {
|
|
1354
1354
|
return {
|
|
1355
1355
|
decision: "block",
|
|
@@ -1375,9 +1375,11 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
|
|
|
1375
1375
|
};
|
|
1376
1376
|
}
|
|
1377
1377
|
if (task.lifecycle?.currentStage !== "running" || task.assignment?.status !== "running") {
|
|
1378
|
+
const lifecycleStage = task.lifecycle?.currentStage ?? "missing";
|
|
1379
|
+
const assignmentStatus = task.assignment?.status ?? "missing";
|
|
1378
1380
|
return {
|
|
1379
1381
|
decision: "block",
|
|
1380
|
-
reason:
|
|
1382
|
+
reason: `Active card must be in Running with a live assignment before product mutation (lifecycle: ${lifecycleStage}; assignment: ${assignmentStatus}). Call kanban start_task after completing the required card details.`,
|
|
1381
1383
|
boardId: board.id,
|
|
1382
1384
|
taskId: task.id
|
|
1383
1385
|
};
|
|
@@ -3607,23 +3609,21 @@ function unwrapDataKey(buf, keyFile) {
|
|
|
3607
3609
|
});
|
|
3608
3610
|
}
|
|
3609
3611
|
}
|
|
3610
|
-
function
|
|
3611
|
-
if (process.platform === "win32") return;
|
|
3612
|
+
function keyFileNeedsHardening(keyFile, opts) {
|
|
3613
|
+
if (process.platform === "win32") return false;
|
|
3612
3614
|
const warn = opts?.warn ?? ((msg) => console.warn(msg));
|
|
3613
3615
|
try {
|
|
3614
3616
|
const stat = fs2.statSync(keyFile);
|
|
3615
3617
|
const actualMode = stat.mode & 511;
|
|
3616
3618
|
if (actualMode !== KEY_FILE_MODE) {
|
|
3617
|
-
void restrictFilePermissions(keyFile, {
|
|
3618
|
-
label: "secret-vault",
|
|
3619
|
-
warn
|
|
3620
|
-
}).catch(() => void 0);
|
|
3621
3619
|
warn(
|
|
3622
3620
|
`Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Hardening\u2026`
|
|
3623
3621
|
);
|
|
3622
|
+
return true;
|
|
3624
3623
|
}
|
|
3625
3624
|
} catch {
|
|
3626
3625
|
}
|
|
3626
|
+
return false;
|
|
3627
3627
|
}
|
|
3628
3628
|
function writeKeyFileAtomicSync(keyFile, content) {
|
|
3629
3629
|
const tmp = `${keyFile}.${randomBytes2(4).toString("hex")}.tmp`;
|
|
@@ -3689,6 +3689,14 @@ var DefaultSecretVault = class {
|
|
|
3689
3689
|
}).catch(() => void 0);
|
|
3690
3690
|
this.pendingHardening.push(p);
|
|
3691
3691
|
}
|
|
3692
|
+
/** Detect and schedule repair of a pre-existing POSIX key with loose mode bits. */
|
|
3693
|
+
checkKeyFilePermissions() {
|
|
3694
|
+
if (keyFileNeedsHardening(this.keyFile, {
|
|
3695
|
+
warn: (msg) => this.logWarn(msg)
|
|
3696
|
+
})) {
|
|
3697
|
+
this.scheduleKeyHardening();
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3692
3700
|
/** Flush all pending key-file hardening promises. */
|
|
3693
3701
|
flushHardening() {
|
|
3694
3702
|
if (this.pendingHardening.length === 0) return Promise.resolve();
|
|
@@ -3776,7 +3784,7 @@ var DefaultSecretVault = class {
|
|
|
3776
3784
|
writeKeyFileAtomicSync(this.keyFile, keyFileBuf);
|
|
3777
3785
|
}
|
|
3778
3786
|
this.scheduleKeyHardening();
|
|
3779
|
-
|
|
3787
|
+
this.checkKeyFilePermissions();
|
|
3780
3788
|
this.key = newKey;
|
|
3781
3789
|
this._keyVersion = newVersion;
|
|
3782
3790
|
return { oldVersion, newVersion };
|
|
@@ -3792,12 +3800,9 @@ var DefaultSecretVault = class {
|
|
|
3792
3800
|
const passphrase = getVaultPassphrase();
|
|
3793
3801
|
if (!passphrase || !this.key) return;
|
|
3794
3802
|
try {
|
|
3795
|
-
writeKeyFileAtomicSync(
|
|
3796
|
-
this.keyFile,
|
|
3797
|
-
wrapDataKey(this.key, this._keyVersion, passphrase)
|
|
3798
|
-
);
|
|
3803
|
+
writeKeyFileAtomicSync(this.keyFile, wrapDataKey(this.key, this._keyVersion, passphrase));
|
|
3799
3804
|
this.scheduleKeyHardening();
|
|
3800
|
-
|
|
3805
|
+
this.checkKeyFilePermissions();
|
|
3801
3806
|
} catch {
|
|
3802
3807
|
}
|
|
3803
3808
|
}
|
|
@@ -3809,13 +3814,13 @@ var DefaultSecretVault = class {
|
|
|
3809
3814
|
const { key: key2, version } = unwrapDataKey(buf, this.keyFile);
|
|
3810
3815
|
this.key = key2;
|
|
3811
3816
|
this._keyVersion = version;
|
|
3812
|
-
|
|
3817
|
+
this.checkKeyFilePermissions();
|
|
3813
3818
|
return this.key;
|
|
3814
3819
|
}
|
|
3815
3820
|
if (buf.length === KEY_BYTES) {
|
|
3816
3821
|
this.key = buf;
|
|
3817
3822
|
this._keyVersion = 1;
|
|
3818
|
-
|
|
3823
|
+
this.checkKeyFilePermissions();
|
|
3819
3824
|
this.migrateToWrappedIfPassphrase();
|
|
3820
3825
|
return this.key;
|
|
3821
3826
|
}
|
|
@@ -3839,7 +3844,7 @@ var DefaultSecretVault = class {
|
|
|
3839
3844
|
}
|
|
3840
3845
|
this.key = Buffer.from(key2);
|
|
3841
3846
|
this._keyVersion = version;
|
|
3842
|
-
|
|
3847
|
+
this.checkKeyFilePermissions();
|
|
3843
3848
|
this.migrateToWrappedIfPassphrase();
|
|
3844
3849
|
return this.key;
|
|
3845
3850
|
}
|
|
@@ -3866,13 +3871,13 @@ var DefaultSecretVault = class {
|
|
|
3866
3871
|
const { key: winnerKey, version } = unwrapDataKey(buf, this.keyFile);
|
|
3867
3872
|
this.key = winnerKey;
|
|
3868
3873
|
this._keyVersion = version;
|
|
3869
|
-
|
|
3874
|
+
this.checkKeyFilePermissions();
|
|
3870
3875
|
return this.key;
|
|
3871
3876
|
}
|
|
3872
3877
|
if (buf.length === KEY_BYTES) {
|
|
3873
3878
|
this.key = buf;
|
|
3874
3879
|
this._keyVersion = 1;
|
|
3875
|
-
|
|
3880
|
+
this.checkKeyFilePermissions();
|
|
3876
3881
|
return this.key;
|
|
3877
3882
|
}
|
|
3878
3883
|
if (buf.length === VERSIONED_KEY_FILE_SIZE) {
|
|
@@ -3888,7 +3893,7 @@ var DefaultSecretVault = class {
|
|
|
3888
3893
|
const winnerKey = buf.subarray(KEY_FILE_MAGIC.length + 1);
|
|
3889
3894
|
this.key = Buffer.from(winnerKey);
|
|
3890
3895
|
this._keyVersion = version;
|
|
3891
|
-
|
|
3896
|
+
this.checkKeyFilePermissions();
|
|
3892
3897
|
return this.key;
|
|
3893
3898
|
}
|
|
3894
3899
|
throw new ConfigError({
|
|
@@ -46,6 +46,8 @@ export declare class DefaultSecretVault implements RotatableSecretVault {
|
|
|
46
46
|
* can never become an unhandled one.
|
|
47
47
|
*/
|
|
48
48
|
private scheduleKeyHardening;
|
|
49
|
+
/** Detect and schedule repair of a pre-existing POSIX key with loose mode bits. */
|
|
50
|
+
private checkKeyFilePermissions;
|
|
49
51
|
/** Flush all pending key-file hardening promises. */
|
|
50
52
|
flushHardening(): Promise<void>;
|
|
51
53
|
/** Current key version. Starts at 1; incremented by rotateKey(). */
|
|
@@ -1198,6 +1198,22 @@ function isPidAlive(pid) {
|
|
|
1198
1198
|
}
|
|
1199
1199
|
}
|
|
1200
1200
|
|
|
1201
|
+
// src/utils/session-scoped-path.ts
|
|
1202
|
+
var SESSION_SIDECAR_JSONL_SUFFIXES = [
|
|
1203
|
+
".replay.jsonl",
|
|
1204
|
+
".audit.jsonl",
|
|
1205
|
+
".annotations.jsonl"
|
|
1206
|
+
];
|
|
1207
|
+
var RESERVED_SESSION_JSONL_NAMES = /* @__PURE__ */ new Set([
|
|
1208
|
+
"_index.jsonl",
|
|
1209
|
+
"_mailbox.jsonl"
|
|
1210
|
+
]);
|
|
1211
|
+
function isSessionTranscriptFileName(name) {
|
|
1212
|
+
if (!name.endsWith(".jsonl")) return false;
|
|
1213
|
+
if (RESERVED_SESSION_JSONL_NAMES.has(name)) return false;
|
|
1214
|
+
return !SESSION_SIDECAR_JSONL_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1201
1217
|
// src/session-catalog/store.ts
|
|
1202
1218
|
var SCHEMA_VERSION = 1;
|
|
1203
1219
|
var MAX_LEASE_MS = 12e4;
|
|
@@ -1683,13 +1699,51 @@ var SessionCatalogStore = class {
|
|
|
1683
1699
|
damaged: row.damaged !== 0
|
|
1684
1700
|
};
|
|
1685
1701
|
}
|
|
1686
|
-
listCatalog(
|
|
1687
|
-
const
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
)
|
|
1702
|
+
listCatalog(criteria = {}) {
|
|
1703
|
+
const requestedLimit = criteria.limit ?? 100;
|
|
1704
|
+
if (!Number.isFinite(requestedLimit)) throw new TypeError("Invalid session catalog limit");
|
|
1705
|
+
const bounded = Math.min(MAX_PAGE, Math.max(1, Math.floor(requestedLimit)));
|
|
1706
|
+
const clauses = [];
|
|
1707
|
+
const values = [];
|
|
1708
|
+
const jsonText = (field) => `CASE WHEN json_valid(summary_json) THEN json_extract(summary_json,'$.${field}') END`;
|
|
1709
|
+
const literalLike = (value) => `%${value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`;
|
|
1710
|
+
const search = criteria.search?.trim();
|
|
1711
|
+
if (search) {
|
|
1712
|
+
const pattern = literalLike(search);
|
|
1713
|
+
clauses.push(
|
|
1714
|
+
`(session_id LIKE ? ESCAPE '\\' OR ${jsonText("title")} LIKE ? ESCAPE '\\' OR ${jsonText("name")} LIKE ? ESCAPE '\\')`
|
|
1715
|
+
);
|
|
1716
|
+
values.push(pattern, pattern, pattern);
|
|
1717
|
+
}
|
|
1718
|
+
if (criteria.since) {
|
|
1719
|
+
clauses.push(`${jsonText("startedAt")}>=?`);
|
|
1720
|
+
values.push(criteria.since);
|
|
1721
|
+
}
|
|
1722
|
+
if (criteria.until) {
|
|
1723
|
+
clauses.push(`${jsonText("startedAt")}<=?`);
|
|
1724
|
+
values.push(criteria.until);
|
|
1725
|
+
}
|
|
1726
|
+
if (criteria.provider) {
|
|
1727
|
+
clauses.push(`${jsonText("provider")}=?`);
|
|
1728
|
+
values.push(criteria.provider);
|
|
1729
|
+
}
|
|
1730
|
+
if (criteria.model) {
|
|
1731
|
+
clauses.push(`${jsonText("model")}=?`);
|
|
1732
|
+
values.push(criteria.model);
|
|
1733
|
+
}
|
|
1734
|
+
if (criteria.minTokens !== void 0) {
|
|
1735
|
+
if (!Number.isFinite(criteria.minTokens)) throw new TypeError("Invalid minimum token count");
|
|
1736
|
+
clauses.push(`CAST(COALESCE(${jsonText("tokenTotal")},0) AS REAL)>=?`);
|
|
1737
|
+
values.push(criteria.minTokens);
|
|
1738
|
+
}
|
|
1739
|
+
if (criteria.titleContains) {
|
|
1740
|
+
clauses.push(`${jsonText("title")} LIKE ? ESCAPE '\\'`);
|
|
1741
|
+
values.push(literalLike(criteria.titleContains.toLocaleLowerCase()));
|
|
1742
|
+
}
|
|
1743
|
+
const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
1744
|
+
const rows = this.db.prepare(
|
|
1745
|
+
`SELECT * FROM sessions${where} ORDER BY COALESCE(${jsonText("lastActivityAt")},${jsonText("endedAt")},${jsonText("startedAt")}) DESC,${jsonText("startedAt")} DESC,session_id ASC LIMIT ?`
|
|
1746
|
+
).all(...values, bounded);
|
|
1693
1747
|
return rows.map((row) => this.catalogRecord(row));
|
|
1694
1748
|
}
|
|
1695
1749
|
getSummary(sessionId) {
|
|
@@ -1844,7 +1898,7 @@ var SessionCatalogStore = class {
|
|
|
1844
1898
|
rebuildCatalog() {
|
|
1845
1899
|
const summaries = this.walkFiles(this.sessionsDir, ".summary.json");
|
|
1846
1900
|
const transcripts = this.walkFiles(this.sessionsDir, ".jsonl").filter(
|
|
1847
|
-
(file) =>
|
|
1901
|
+
(file) => isSessionTranscriptFileName(path4.basename(file))
|
|
1848
1902
|
);
|
|
1849
1903
|
const ids = /* @__PURE__ */ new Set();
|
|
1850
1904
|
for (const file of [...summaries, ...transcripts]) {
|
|
@@ -567,6 +567,22 @@ function isPidAlive(pid) {
|
|
|
567
567
|
}
|
|
568
568
|
}
|
|
569
569
|
|
|
570
|
+
// src/utils/session-scoped-path.ts
|
|
571
|
+
var SESSION_SIDECAR_JSONL_SUFFIXES = [
|
|
572
|
+
".replay.jsonl",
|
|
573
|
+
".audit.jsonl",
|
|
574
|
+
".annotations.jsonl"
|
|
575
|
+
];
|
|
576
|
+
var RESERVED_SESSION_JSONL_NAMES = /* @__PURE__ */ new Set([
|
|
577
|
+
"_index.jsonl",
|
|
578
|
+
"_mailbox.jsonl"
|
|
579
|
+
]);
|
|
580
|
+
function isSessionTranscriptFileName(name) {
|
|
581
|
+
if (!name.endsWith(".jsonl")) return false;
|
|
582
|
+
if (RESERVED_SESSION_JSONL_NAMES.has(name)) return false;
|
|
583
|
+
return !SESSION_SIDECAR_JSONL_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
584
|
+
}
|
|
585
|
+
|
|
570
586
|
// src/session-catalog/store.ts
|
|
571
587
|
var SCHEMA_VERSION = 1;
|
|
572
588
|
var MAX_LEASE_MS = 12e4;
|
|
@@ -1052,13 +1068,51 @@ var SessionCatalogStore = class {
|
|
|
1052
1068
|
damaged: row.damaged !== 0
|
|
1053
1069
|
};
|
|
1054
1070
|
}
|
|
1055
|
-
listCatalog(
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
)
|
|
1071
|
+
listCatalog(criteria = {}) {
|
|
1072
|
+
const requestedLimit = criteria.limit ?? 100;
|
|
1073
|
+
if (!Number.isFinite(requestedLimit)) throw new TypeError("Invalid session catalog limit");
|
|
1074
|
+
const bounded = Math.min(MAX_PAGE, Math.max(1, Math.floor(requestedLimit)));
|
|
1075
|
+
const clauses = [];
|
|
1076
|
+
const values = [];
|
|
1077
|
+
const jsonText = (field) => `CASE WHEN json_valid(summary_json) THEN json_extract(summary_json,'$.${field}') END`;
|
|
1078
|
+
const literalLike = (value) => `%${value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`;
|
|
1079
|
+
const search = criteria.search?.trim();
|
|
1080
|
+
if (search) {
|
|
1081
|
+
const pattern = literalLike(search);
|
|
1082
|
+
clauses.push(
|
|
1083
|
+
`(session_id LIKE ? ESCAPE '\\' OR ${jsonText("title")} LIKE ? ESCAPE '\\' OR ${jsonText("name")} LIKE ? ESCAPE '\\')`
|
|
1084
|
+
);
|
|
1085
|
+
values.push(pattern, pattern, pattern);
|
|
1086
|
+
}
|
|
1087
|
+
if (criteria.since) {
|
|
1088
|
+
clauses.push(`${jsonText("startedAt")}>=?`);
|
|
1089
|
+
values.push(criteria.since);
|
|
1090
|
+
}
|
|
1091
|
+
if (criteria.until) {
|
|
1092
|
+
clauses.push(`${jsonText("startedAt")}<=?`);
|
|
1093
|
+
values.push(criteria.until);
|
|
1094
|
+
}
|
|
1095
|
+
if (criteria.provider) {
|
|
1096
|
+
clauses.push(`${jsonText("provider")}=?`);
|
|
1097
|
+
values.push(criteria.provider);
|
|
1098
|
+
}
|
|
1099
|
+
if (criteria.model) {
|
|
1100
|
+
clauses.push(`${jsonText("model")}=?`);
|
|
1101
|
+
values.push(criteria.model);
|
|
1102
|
+
}
|
|
1103
|
+
if (criteria.minTokens !== void 0) {
|
|
1104
|
+
if (!Number.isFinite(criteria.minTokens)) throw new TypeError("Invalid minimum token count");
|
|
1105
|
+
clauses.push(`CAST(COALESCE(${jsonText("tokenTotal")},0) AS REAL)>=?`);
|
|
1106
|
+
values.push(criteria.minTokens);
|
|
1107
|
+
}
|
|
1108
|
+
if (criteria.titleContains) {
|
|
1109
|
+
clauses.push(`${jsonText("title")} LIKE ? ESCAPE '\\'`);
|
|
1110
|
+
values.push(literalLike(criteria.titleContains.toLocaleLowerCase()));
|
|
1111
|
+
}
|
|
1112
|
+
const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
1113
|
+
const rows = this.db.prepare(
|
|
1114
|
+
`SELECT * FROM sessions${where} ORDER BY COALESCE(${jsonText("lastActivityAt")},${jsonText("endedAt")},${jsonText("startedAt")}) DESC,${jsonText("startedAt")} DESC,session_id ASC LIMIT ?`
|
|
1115
|
+
).all(...values, bounded);
|
|
1062
1116
|
return rows.map((row) => this.catalogRecord(row));
|
|
1063
1117
|
}
|
|
1064
1118
|
getSummary(sessionId) {
|
|
@@ -1213,7 +1267,7 @@ var SessionCatalogStore = class {
|
|
|
1213
1267
|
rebuildCatalog() {
|
|
1214
1268
|
const summaries = this.walkFiles(this.sessionsDir, ".summary.json");
|
|
1215
1269
|
const transcripts = this.walkFiles(this.sessionsDir, ".jsonl").filter(
|
|
1216
|
-
(file) =>
|
|
1270
|
+
(file) => isSessionTranscriptFileName(path2.basename(file))
|
|
1217
1271
|
);
|
|
1218
1272
|
const ids = /* @__PURE__ */ new Set();
|
|
1219
1273
|
for (const file of [...summaries, ...transcripts]) {
|
|
@@ -1422,7 +1476,16 @@ var OPERATION_KEYS = {
|
|
|
1422
1476
|
subscribe: ["cursor"],
|
|
1423
1477
|
unsubscribe: [],
|
|
1424
1478
|
upsert_summary: ["summary", "transcriptRelativePath", "summaryRelativePath"],
|
|
1425
|
-
list_catalog: [
|
|
1479
|
+
list_catalog: [
|
|
1480
|
+
"limit",
|
|
1481
|
+
"search",
|
|
1482
|
+
"since",
|
|
1483
|
+
"until",
|
|
1484
|
+
"provider",
|
|
1485
|
+
"model",
|
|
1486
|
+
"minTokens",
|
|
1487
|
+
"titleContains"
|
|
1488
|
+
],
|
|
1426
1489
|
resolve_id: ["query"],
|
|
1427
1490
|
get_summary: ["sessionId"],
|
|
1428
1491
|
rename: ["sessionId", "name"],
|
|
@@ -1580,10 +1643,7 @@ async function dispatch(op, args) {
|
|
|
1580
1643
|
}
|
|
1581
1644
|
case "list_catalog": {
|
|
1582
1645
|
const value = args;
|
|
1583
|
-
return catalog.listCatalog(
|
|
1584
|
-
value.limit,
|
|
1585
|
-
value.search
|
|
1586
|
-
);
|
|
1646
|
+
return catalog.listCatalog(value);
|
|
1587
1647
|
}
|
|
1588
1648
|
case "resolve_id":
|
|
1589
1649
|
return catalog.resolveId(
|
|
@@ -67,6 +67,16 @@ export interface CatalogSessionRecord extends SessionSummary {
|
|
|
67
67
|
indexedAt: string;
|
|
68
68
|
damaged: boolean;
|
|
69
69
|
}
|
|
70
|
+
export interface SessionCatalogListArgs {
|
|
71
|
+
limit?: number | undefined;
|
|
72
|
+
search?: string | undefined;
|
|
73
|
+
since?: string | undefined;
|
|
74
|
+
until?: string | undefined;
|
|
75
|
+
provider?: string | undefined;
|
|
76
|
+
model?: string | undefined;
|
|
77
|
+
minTokens?: number | undefined;
|
|
78
|
+
titleContains?: string | undefined;
|
|
79
|
+
}
|
|
70
80
|
export type SessionCatalogEventKind = 'session.claimed' | 'session.presence_changed' | 'session.closing' | 'session.released' | 'session.catalog_changed' | 'session.deleted' | 'session.rebuild_started' | 'session.rebuild_completed';
|
|
71
81
|
export interface SessionCatalogEvent {
|
|
72
82
|
instanceId: string;
|
|
@@ -185,10 +195,7 @@ export interface SessionCatalogOperations {
|
|
|
185
195
|
result: CatalogSessionRecord;
|
|
186
196
|
};
|
|
187
197
|
list_catalog: {
|
|
188
|
-
args:
|
|
189
|
-
limit?: number;
|
|
190
|
-
search?: string;
|
|
191
|
-
};
|
|
198
|
+
args: SessionCatalogListArgs;
|
|
192
199
|
result: CatalogSessionRecord[];
|
|
193
200
|
};
|
|
194
201
|
resolve_id: {
|