@gajae-code/coding-agent 0.15.4 → 0.15.5
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 +20 -1
- package/dist/types/cli/update-cli.d.ts +25 -5
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/cli/session-cli.d.ts +42 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
- package/dist/types/tools/path-utils.d.ts +5 -6
- package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
- package/package.json +7 -7
- package/src/cli/update-cli.ts +102 -1
- package/src/commands/sdk.ts +5 -0
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +10 -4
- package/src/coordinator-mcp/policy.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/launch-worktree.ts +44 -7
- package/src/gjc-runtime/ralplan-runtime.ts +14 -1
- package/src/gjc-runtime/tmux-sessions.ts +145 -48
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/modes/prompt-action-autocomplete.ts +26 -47
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/kind-aware-reconciliation.ts +10 -2
- package/src/sdk/bus/prompt-reconciliation.ts +5 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/cli/session-cli.ts +149 -4
- package/src/sdk/host/host.ts +7 -0
- package/src/sdk/host/session-runtime.test.ts +821 -22
- package/src/sdk/host/session-runtime.ts +669 -67
- package/src/sdk/router/session-router.ts +69 -15
- package/src/session/agent-session.ts +232 -15
- package/src/session/internal/managed-session-storage.ts +64 -26
- package/src/session/session-manager.ts +26 -8
- package/src/tools/bash.ts +14 -0
- package/src/tools/path-utils.ts +77 -29
- package/src/tools/tmux-self-injection-guard.ts +359 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AutocompleteItem,
|
|
3
3
|
type AutocompleteProvider,
|
|
4
|
+
autocompleteFuzzyMatch,
|
|
5
|
+
autocompleteFuzzyScore,
|
|
4
6
|
CombinedAutocompleteProvider,
|
|
5
7
|
extractSlashCommandTokenPrefix,
|
|
6
8
|
getSlashCommandMatchRank,
|
|
9
|
+
normalizeFuzzyText,
|
|
7
10
|
type SlashCommand,
|
|
8
11
|
} from "@gajae-code/tui";
|
|
9
12
|
import type { KeybindingsManager } from "../config/keybindings";
|
|
@@ -50,43 +53,6 @@ interface PromptActionAutocompleteOptions {
|
|
|
50
53
|
getPromptSuggestion?: () => string | null;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
function fuzzyMatch(query: string, target: string): boolean {
|
|
54
|
-
if (query.length === 0) return true;
|
|
55
|
-
if (query.length > target.length) return false;
|
|
56
|
-
|
|
57
|
-
let queryIndex = 0;
|
|
58
|
-
for (let targetIndex = 0; targetIndex < target.length && queryIndex < query.length; targetIndex += 1) {
|
|
59
|
-
if (query[queryIndex] === target[targetIndex]) {
|
|
60
|
-
queryIndex += 1;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return queryIndex === query.length;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function fuzzyScore(query: string, target: string): number {
|
|
68
|
-
if (query.length === 0) return 1;
|
|
69
|
-
if (target === query) return 100;
|
|
70
|
-
if (target.startsWith(query)) return 80;
|
|
71
|
-
if (target.includes(query)) return 60;
|
|
72
|
-
|
|
73
|
-
let queryIndex = 0;
|
|
74
|
-
let gaps = 0;
|
|
75
|
-
let lastMatchIndex = -1;
|
|
76
|
-
for (let targetIndex = 0; targetIndex < target.length && queryIndex < query.length; targetIndex += 1) {
|
|
77
|
-
if (query[queryIndex] === target[targetIndex]) {
|
|
78
|
-
if (lastMatchIndex >= 0 && targetIndex - lastMatchIndex > 1) {
|
|
79
|
-
gaps += 1;
|
|
80
|
-
}
|
|
81
|
-
lastMatchIndex = targetIndex;
|
|
82
|
-
queryIndex += 1;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (queryIndex !== query.length) return 0;
|
|
87
|
-
return Math.max(1, 40 - gaps * 5);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
56
|
function isPromptActionItem(item: AutocompleteItem): item is PromptActionAutocompleteItem {
|
|
91
57
|
return "actionId" in item && "execute" in item && typeof item.execute === "function";
|
|
92
58
|
}
|
|
@@ -127,6 +93,7 @@ function sortSlashCommandSuggestions(
|
|
|
127
93
|
): { items: AutocompleteItem[]; prefix: string } | null {
|
|
128
94
|
if (!suggestions) return null;
|
|
129
95
|
const query = suggestions.prefix.slice(1).toLowerCase();
|
|
96
|
+
const normalizedQuery = normalizeFuzzyText(query);
|
|
130
97
|
const commandIndexes = new Map(commands.map((command, index) => [command.name, index]));
|
|
131
98
|
const commandByName = new Map(commands.map(command => [command.name, command]));
|
|
132
99
|
const items = suggestions.items
|
|
@@ -135,13 +102,20 @@ function sortSlashCommandSuggestions(
|
|
|
135
102
|
const commandIndex = commandIndexes.get(item.value) ?? index;
|
|
136
103
|
const lowerName = item.value.toLowerCase();
|
|
137
104
|
const lowerDesc = command?.description?.toLowerCase() ?? item.description?.toLowerCase() ?? "";
|
|
138
|
-
const
|
|
139
|
-
const
|
|
105
|
+
const normalizedName = normalizeFuzzyText(item.value);
|
|
106
|
+
const normalizedDesc = normalizeFuzzyText(lowerDesc);
|
|
107
|
+
const nameScore = autocompleteFuzzyMatch(normalizedQuery, normalizedName)
|
|
108
|
+
? autocompleteFuzzyScore(normalizedQuery, normalizedName)
|
|
109
|
+
: 0;
|
|
110
|
+
const descScore = autocompleteFuzzyMatch(normalizedQuery, normalizedDesc)
|
|
111
|
+
? autocompleteFuzzyScore(normalizedQuery, normalizedDesc) * 0.5
|
|
112
|
+
: 0;
|
|
113
|
+
const isAscii = /^[\x00-\x7F]*$/.test(`${query}${item.value}`);
|
|
140
114
|
return {
|
|
141
115
|
item,
|
|
142
116
|
index,
|
|
143
117
|
commandIndex,
|
|
144
|
-
matchRank: getSlashCommandMatchRank(query, lowerName),
|
|
118
|
+
matchRank: isAscii ? getSlashCommandMatchRank(query, lowerName) : 4,
|
|
145
119
|
priority: getSlashCommandPriority(command, item),
|
|
146
120
|
score: Math.max(nameScore, descScore),
|
|
147
121
|
};
|
|
@@ -219,14 +193,14 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
219
193
|
const searchable = [action.id, action.label, action.description, ...action.keywords]
|
|
220
194
|
.join(" ")
|
|
221
195
|
.toLowerCase();
|
|
222
|
-
if (!
|
|
196
|
+
if (!autocompleteFuzzyMatch(query, searchable)) return null;
|
|
223
197
|
return {
|
|
224
198
|
value: action.label,
|
|
225
199
|
label: action.label,
|
|
226
200
|
description: action.description,
|
|
227
201
|
actionId: action.id,
|
|
228
202
|
execute: action.execute,
|
|
229
|
-
score:
|
|
203
|
+
score: autocompleteFuzzyScore(query, searchable),
|
|
230
204
|
} satisfies PromptActionAutocompleteItem & { score: number };
|
|
231
205
|
})
|
|
232
206
|
.filter(item => item !== null)
|
|
@@ -350,7 +324,10 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
350
324
|
if (!prefix) return null;
|
|
351
325
|
const query = prefix.slice(1).toLowerCase();
|
|
352
326
|
if (query.length === 0 && !options.includeEmpty) return null;
|
|
353
|
-
const normalizedQuery =
|
|
327
|
+
const normalizedQuery = normalizeFuzzyText(
|
|
328
|
+
query.startsWith("skill-") ? `skill:${query.slice("skill-".length)}` : query,
|
|
329
|
+
);
|
|
330
|
+
if (query.length > 0 && normalizedQuery.length === 0) return null;
|
|
354
331
|
const exactNonSkillCommand = this.#commands.some(
|
|
355
332
|
command => command.name === query && !command.name.startsWith("skill:"),
|
|
356
333
|
);
|
|
@@ -364,16 +341,18 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
364
341
|
`skill-${skillName}`,
|
|
365
342
|
...(exactNonSkillCommand ? [] : [skillName]),
|
|
366
343
|
command.description ?? "",
|
|
367
|
-
].map(target => target
|
|
344
|
+
].map(target => normalizeFuzzyText(target));
|
|
368
345
|
if (
|
|
369
|
-
!searchTargets.some(
|
|
346
|
+
!searchTargets.some(
|
|
347
|
+
target => autocompleteFuzzyMatch(normalizedQuery, target) || target.includes(normalizedQuery),
|
|
348
|
+
)
|
|
370
349
|
) {
|
|
371
350
|
return null;
|
|
372
351
|
}
|
|
373
352
|
const bestScore = Math.max(
|
|
374
353
|
...searchTargets.map(target =>
|
|
375
|
-
|
|
376
|
-
?
|
|
354
|
+
autocompleteFuzzyMatch(normalizedQuery, target)
|
|
355
|
+
? autocompleteFuzzyScore(normalizedQuery, target)
|
|
377
356
|
: target.includes(normalizedQuery)
|
|
378
357
|
? 60
|
|
379
358
|
: 0,
|
|
@@ -132,8 +132,8 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
132
132
|
* would retain the old hot polling loop, so replacement is required.
|
|
133
133
|
*/
|
|
134
134
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
135
|
-
discord:
|
|
136
|
-
slack:
|
|
135
|
+
discord: 70,
|
|
136
|
+
slack: 73,
|
|
137
137
|
};
|
|
138
138
|
|
|
139
139
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|
|
@@ -355,7 +355,13 @@ export function createKindAwareReconciliation(
|
|
|
355
355
|
// as-is (cleanupRecords is intentionally not re-run). First reason wins: a
|
|
356
356
|
// late generic frame never overwrites a specific one. Persisted so the reason
|
|
357
357
|
// survives reconnect/restart reconciliation.
|
|
358
|
-
if (frame.type === "agent_failed"
|
|
358
|
+
if (frame.type === "agent_failed") {
|
|
359
|
+
const failure = sanitizePromptFailure(frame.error);
|
|
360
|
+
if (
|
|
361
|
+
record.error !== undefined &&
|
|
362
|
+
!(record.error.code === "agent_failed" && failure.code !== "agent_failed")
|
|
363
|
+
)
|
|
364
|
+
return { value: undefined, changed: false };
|
|
359
365
|
record.error = sanitizePromptFailure(frame.error);
|
|
360
366
|
return { value: undefined, changed: true };
|
|
361
367
|
}
|
|
@@ -373,7 +379,9 @@ export function createKindAwareReconciliation(
|
|
|
373
379
|
if (frame.type === "agent_failed") {
|
|
374
380
|
// agent_failed is additive diagnostics; agent_end remains the sole
|
|
375
381
|
// terminal lifecycle boundary for the correlated invocation.
|
|
376
|
-
|
|
382
|
+
const failure = sanitizePromptFailure(frame.error);
|
|
383
|
+
if (record.error === undefined || (record.error.code === "agent_failed" && failure.code !== "agent_failed"))
|
|
384
|
+
record.error = failure;
|
|
377
385
|
delete record.deadlineRecoveryPending;
|
|
378
386
|
delete record.deadlineMaxAt;
|
|
379
387
|
return { value: undefined, changed: true };
|
|
@@ -202,8 +202,11 @@ export function createPromptReconciliation(options: { now?: () => number } = {})
|
|
|
202
202
|
// the settled record instead of being dropped; it must not resurrect it,
|
|
203
203
|
// so status, terminalAt, retention order, and clientRefIndex stay as-is.
|
|
204
204
|
// First reason wins: a late generic frame never overwrites a specific one.
|
|
205
|
-
if (frame.type === "agent_failed"
|
|
206
|
-
|
|
205
|
+
if (frame.type === "agent_failed") {
|
|
206
|
+
const failure = sanitizePromptFailure(frame.error);
|
|
207
|
+
if (record.error === undefined || (record.error.code === "agent_failed" && failure.code !== "agent_failed"))
|
|
208
|
+
record.error = failure;
|
|
209
|
+
}
|
|
207
210
|
return;
|
|
208
211
|
}
|
|
209
212
|
if (frame.type === "agent_start") {
|
|
@@ -283,8 +283,15 @@ export const SDK_LIFECYCLE_ROUTER_PROTOCOL_VERSION = 1;
|
|
|
283
283
|
* Generation 177 consumes an exact same-window lean settlement receipt only
|
|
284
284
|
* after its autonomous ask lead-in publishes successfully, preventing idle
|
|
285
285
|
* replay without changing the serving protocol.
|
|
286
|
+
* Generation 178: idle publication waits for positioned identity delivery to
|
|
287
|
+
* cross the native writer barrier before the independent broadcast lane.
|
|
288
|
+
* Generation 179 tolerates a bounded consecutive run of refused notification
|
|
289
|
+
* publications in the router instead of cancelling the subscription on the
|
|
290
|
+
* first refusal, so an owner still running the generation-178 router keeps
|
|
291
|
+
* killing a session's mirroring after one transient rejection and must be
|
|
292
|
+
* replaced across this upgrade.
|
|
286
293
|
*/
|
|
287
|
-
export const DAEMON_GENERATION =
|
|
294
|
+
export const DAEMON_GENERATION = 179;
|
|
288
295
|
|
|
289
296
|
/**
|
|
290
297
|
* Serving-compatibility boundary for daemon lifecycle requests. Epoch 7
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import * as fsSync from "node:fs";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
4
5
|
import { getAgentDir } from "@gajae-code/utils";
|
|
6
|
+
import { repo as resolveGitRepository } from "../../utils/git";
|
|
5
7
|
import { ensureBroker } from "../broker/ensure";
|
|
6
8
|
import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
|
|
7
9
|
import { SdkClientError } from "../client";
|
|
@@ -38,10 +40,12 @@ export type SdkSessionCliAction =
|
|
|
38
40
|
| "status"
|
|
39
41
|
| "tail"
|
|
40
42
|
| "retire"
|
|
43
|
+
| "global"
|
|
41
44
|
| "raw"
|
|
42
45
|
| "control"
|
|
43
46
|
| "query"
|
|
44
47
|
| "global";
|
|
48
|
+
export type SdkSessionListScope = "repo" | "cwd" | "worktree" | "all";
|
|
45
49
|
export type SdkSessionCliRawKind = "control" | "query" | "global";
|
|
46
50
|
|
|
47
51
|
export interface SdkSessionCliArgs {
|
|
@@ -56,6 +60,7 @@ export interface SdkSessionCliArgs {
|
|
|
56
60
|
jsonInputFile?: string;
|
|
57
61
|
jsonInputStdin?: boolean;
|
|
58
62
|
idempotencyKey?: string;
|
|
63
|
+
scope?: string;
|
|
59
64
|
confirm?: boolean;
|
|
60
65
|
cursor?: string;
|
|
61
66
|
wait?: boolean;
|
|
@@ -358,16 +363,156 @@ async function sessionRows(agentDir: string): Promise<SessionRows> {
|
|
|
358
363
|
});
|
|
359
364
|
}
|
|
360
365
|
|
|
361
|
-
|
|
366
|
+
const SESSION_LIST_SCOPES: readonly SdkSessionListScope[] = ["repo", "cwd", "worktree", "all"];
|
|
367
|
+
/** Row workspaces whose locator cannot be canonicalized still get one deterministic identity. */
|
|
368
|
+
const WORKSPACE_IDENTITY_CACHE_LIMIT = 4096;
|
|
369
|
+
|
|
370
|
+
interface WorkspaceIdentity {
|
|
371
|
+
/** Canonical workspace path (realpath when it exists, lexical resolve otherwise). */
|
|
372
|
+
canonicalPath: string;
|
|
373
|
+
/** Canonical containing worktree root; null outside a Git checkout. */
|
|
374
|
+
repoRoot: string | null;
|
|
375
|
+
/** Canonical Git common dir; shared by the main checkout and linked worktrees. */
|
|
376
|
+
commonDir: string | null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export interface SdkSessionListSelection {
|
|
380
|
+
scope: SdkSessionListScope;
|
|
381
|
+
selection: WorkspaceIdentity;
|
|
382
|
+
/** Bounded, credential-free descriptor included in list output. */
|
|
383
|
+
descriptor: {
|
|
384
|
+
scope: SdkSessionListScope;
|
|
385
|
+
path: string;
|
|
386
|
+
worktreeRoot?: string;
|
|
387
|
+
commonDir?: string;
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Parses `--scope`; missing means `repo`, anything else invalid is a usage error. */
|
|
392
|
+
export function parseSessionListScope(value: string | undefined): SdkSessionListScope {
|
|
393
|
+
if (value === undefined) return "repo";
|
|
394
|
+
if ((SESSION_LIST_SCOPES as readonly string[]).includes(value)) return value as SdkSessionListScope;
|
|
395
|
+
throw new SdkSessionCliError(
|
|
396
|
+
"usage",
|
|
397
|
+
`Invalid scope "${value}". Expected one of: ${SESSION_LIST_SCOPES.join(", ")}.`,
|
|
398
|
+
2,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function canonicalWorkspacePath(target: string): Promise<string> {
|
|
403
|
+
try {
|
|
404
|
+
return await fs.realpath(target);
|
|
405
|
+
} catch {
|
|
406
|
+
// A removed or unreadable workspace keeps a deterministic lexical identity
|
|
407
|
+
// instead of guessing a broader or narrower one.
|
|
408
|
+
return path.resolve(target);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const workspaceIdentityCache = new Map<string, WorkspaceIdentity>();
|
|
413
|
+
|
|
414
|
+
/** Resolves (and caches) the Git identity of one workspace locator. */
|
|
415
|
+
async function workspaceIdentity(target: string): Promise<WorkspaceIdentity> {
|
|
416
|
+
const canonicalPath = await canonicalWorkspacePath(target);
|
|
417
|
+
const cached = workspaceIdentityCache.get(canonicalPath);
|
|
418
|
+
if (cached) return cached;
|
|
419
|
+
if (target === "unknown") {
|
|
420
|
+
const unavailable = { canonicalPath, repoRoot: null, commonDir: null };
|
|
421
|
+
workspaceIdentityCache.set(canonicalPath, unavailable);
|
|
422
|
+
return unavailable;
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
if (!(await fs.stat(target)).isDirectory()) throw new Error("workspace is not a directory");
|
|
426
|
+
} catch {
|
|
427
|
+
// A removed or unreadable locator cannot prove Git membership. Keeping it
|
|
428
|
+
// outside every identity is the narrowest safe result for row filtering.
|
|
429
|
+
const unavailable = { canonicalPath, repoRoot: null, commonDir: null };
|
|
430
|
+
workspaceIdentityCache.set(canonicalPath, unavailable);
|
|
431
|
+
return unavailable;
|
|
432
|
+
}
|
|
433
|
+
const repository = await resolveGitRepository.resolve(canonicalPath);
|
|
434
|
+
const identity: WorkspaceIdentity = repository
|
|
435
|
+
? {
|
|
436
|
+
canonicalPath,
|
|
437
|
+
repoRoot: await canonicalWorkspacePath(repository.repoRoot),
|
|
438
|
+
commonDir: await canonicalWorkspacePath(repository.commonDir),
|
|
439
|
+
}
|
|
440
|
+
: { canonicalPath, repoRoot: null, commonDir: null };
|
|
441
|
+
if (workspaceIdentityCache.size >= WORKSPACE_IDENTITY_CACHE_LIMIT) workspaceIdentityCache.clear();
|
|
442
|
+
workspaceIdentityCache.set(canonicalPath, identity);
|
|
443
|
+
return identity;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Resolves the list selection from `--repo` (default: process cwd) under the
|
|
448
|
+
* effective scope. Outside Git, `repo`/`worktree` fail typed and actionable —
|
|
449
|
+
* they never broaden to `all`; `cwd` remains an exact canonical match.
|
|
450
|
+
*/
|
|
451
|
+
export async function resolveSessionListSelection(
|
|
452
|
+
scope: SdkSessionListScope,
|
|
453
|
+
repoArg: string | undefined,
|
|
454
|
+
): Promise<SdkSessionListSelection> {
|
|
455
|
+
const selection = await workspaceIdentity(repoArg ?? process.cwd());
|
|
456
|
+
if (scope !== "cwd" && !selection.repoRoot)
|
|
457
|
+
throw new SdkSessionCliError(
|
|
458
|
+
"not_a_repository",
|
|
459
|
+
`--scope ${scope} requires a Git repository, but "${selection.canonicalPath}" is outside any Git checkout. ` +
|
|
460
|
+
"Use --scope cwd for an exact workspace match or --scope all for the full broker listing.",
|
|
461
|
+
1,
|
|
462
|
+
);
|
|
463
|
+
const descriptor: SdkSessionListSelection["descriptor"] = {
|
|
464
|
+
scope,
|
|
465
|
+
path: selection.canonicalPath,
|
|
466
|
+
...(selection.repoRoot ? { worktreeRoot: selection.repoRoot, commonDir: selection.commonDir! } : {}),
|
|
467
|
+
};
|
|
468
|
+
return { scope, selection, descriptor };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Filters fully traversed broker rows by the selection scope. Row workspaces
|
|
473
|
+
* are canonicalized and cached per distinct locator; `repo` matches the shared
|
|
474
|
+
* common dir across the main checkout and linked worktrees, `worktree` the
|
|
475
|
+
* containing worktree only, and `cwd` the exact canonical workspace.
|
|
476
|
+
*/
|
|
477
|
+
export async function filterSessionRowsByScope(
|
|
478
|
+
rows: readonly SdkSessionRowV1[],
|
|
479
|
+
scope: SdkSessionListScope,
|
|
480
|
+
selection: SdkSessionListSelection,
|
|
481
|
+
): Promise<{ sessions: SdkSessionRowV1[]; warnings: string[] }> {
|
|
482
|
+
if (scope === "all") return { sessions: [...rows], warnings: [] };
|
|
483
|
+
const warnings: string[] = [];
|
|
484
|
+
const sessions: SdkSessionRowV1[] = [];
|
|
485
|
+
for (const row of rows) {
|
|
486
|
+
const identity = await workspaceIdentity(row.locator.repo);
|
|
487
|
+
let keep: boolean;
|
|
488
|
+
if (scope === "cwd") keep = identity.canonicalPath === selection.selection.canonicalPath;
|
|
489
|
+
else if (scope === "worktree")
|
|
490
|
+
keep = identity.repoRoot !== null && identity.repoRoot === selection.selection.repoRoot;
|
|
491
|
+
else keep = identity.commonDir !== null && identity.commonDir === selection.selection.commonDir;
|
|
492
|
+
if (keep) sessions.push(row);
|
|
493
|
+
else if (identity.repoRoot === null && identity.commonDir === null)
|
|
494
|
+
warnings.push(
|
|
495
|
+
`Session ${row.sessionId} workspace "${row.locator.repo}" is outside Git; excluded by scope ${scope}.`,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
return { sessions, warnings };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function runList(agentDir: string, args: SdkSessionCliArgs): Promise<unknown> {
|
|
502
|
+
const scope = parseSessionListScope(args.scope);
|
|
503
|
+
const { selection, descriptor } = await resolveSessionListSelection(scope, args.repo);
|
|
362
504
|
const listing = await sessionRows(agentDir);
|
|
505
|
+
const filtered = await filterSessionRowsByScope(listing.sessions, scope, { scope, selection, descriptor });
|
|
363
506
|
return {
|
|
364
507
|
ok: true,
|
|
365
508
|
result: {
|
|
366
509
|
version: SESSION_ROWS_VERSION,
|
|
367
510
|
source: "broker",
|
|
511
|
+
scope,
|
|
512
|
+
selection: descriptor,
|
|
368
513
|
...(listing.indexSeq === undefined ? {} : { indexSeq: listing.indexSeq }),
|
|
369
|
-
sessions:
|
|
370
|
-
warnings: listing.warnings,
|
|
514
|
+
sessions: filtered.sessions,
|
|
515
|
+
warnings: [...listing.warnings, ...filtered.warnings],
|
|
371
516
|
},
|
|
372
517
|
};
|
|
373
518
|
}
|
|
@@ -1367,7 +1512,7 @@ export async function runSdkSessionCli(
|
|
|
1367
1512
|
);
|
|
1368
1513
|
const agentDir = args.agentDir ?? getAgentDir();
|
|
1369
1514
|
if (action === "list") {
|
|
1370
|
-
writeOutput(stripSecretFields(await runList(agentDir)));
|
|
1515
|
+
writeOutput(stripSecretFields(await runList(agentDir, args)));
|
|
1371
1516
|
return;
|
|
1372
1517
|
}
|
|
1373
1518
|
if (action === "inspect") {
|
package/src/sdk/host/host.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface SessionSdkHostOptions extends HostEndpointAdapters {
|
|
|
49
49
|
query?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
|
|
50
50
|
/** Test/lifecycle seam invoked synchronously before fire-and-forget dispatch. */
|
|
51
51
|
onFrameAdmitted?: (connectionId: string, frame: SdkFrame) => void;
|
|
52
|
+
/** Synchronously rejects external input while a runtime is being replaced. */
|
|
53
|
+
onFrameAdmission?: (connectionId: string, frame: SdkFrame) => SdkFrame | undefined;
|
|
52
54
|
/** Best-effort diagnostic observation of accepted control/query frames. */
|
|
53
55
|
onRequest?: SdkRequestObserver;
|
|
54
56
|
/** Runs before a control response is sent; identity transitions use sendTerminal. */
|
|
@@ -408,6 +410,11 @@ export class SessionSdkHost {
|
|
|
408
410
|
|
|
409
411
|
async #onFrame(connectionId: string, frame: SdkFrame): Promise<void> {
|
|
410
412
|
try {
|
|
413
|
+
const admissionRejection = this.#options.onFrameAdmission?.(connectionId, frame);
|
|
414
|
+
if (admissionRejection !== undefined) {
|
|
415
|
+
await this.#send(connectionId, admissionRejection);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
411
418
|
switch (frame.type) {
|
|
412
419
|
case "control_request": {
|
|
413
420
|
this.#observeRequest("control", connectionId, frame);
|