@gajae-code/coding-agent 0.11.5 → 0.11.6
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/types/config/settings-schema.d.ts +11 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -3
- package/dist/types/session/session-manager.d.ts +10 -0
- package/dist/types/session/yield-queue.d.ts +8 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +1 -0
- package/src/config/settings-schema.ts +14 -1
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +1 -1
- package/src/modes/controllers/command-controller.ts +15 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -2
- package/src/modes/runtime-init.ts +16 -2
- package/src/sdk/bus/chat-daemon-control.ts +19 -7
- package/src/sdk/bus/index.ts +4 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -3
- package/src/sdk/bus/telegram-daemon-control.ts +24 -6
- package/src/sdk/bus/telegram-daemon.ts +112 -2
- package/src/sdk/host/control/dispatch.ts +14 -5
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/session/agent-session.ts +972 -619
- package/src/session/session-manager.ts +66 -0
- package/src/session/yield-queue.ts +20 -0
- package/src/slash-commands/builtin-registry.ts +36 -0
- package/src/tools/ask-answer-registry.ts +15 -1
|
@@ -853,6 +853,32 @@ function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | un
|
|
|
853
853
|
return path.resolve(sessionDir) === path.resolve(resolved.scope.directoryPath) ? sessionsRoot : undefined;
|
|
854
854
|
}
|
|
855
855
|
|
|
856
|
+
/**
|
|
857
|
+
* Resolve `target` through the real path of its nearest existing ancestor,
|
|
858
|
+
* re-appending any not-yet-created trailing segments. Symlink/reparse components
|
|
859
|
+
* in the existing prefix are canonicalized (so aliases cannot escape a containment
|
|
860
|
+
* check or alias another file), while a missing leaf (e.g. an uncommitted session
|
|
861
|
+
* transcript) is still resolved deterministically. Non-ENOENT errors (ELOOP,
|
|
862
|
+
* EACCES, ...) surface rather than silently falling back to the lexical path.
|
|
863
|
+
*/
|
|
864
|
+
function canonicalizeThroughExistingAncestor(target: string): string {
|
|
865
|
+
const resolved = path.resolve(target);
|
|
866
|
+
const tail: string[] = [];
|
|
867
|
+
let current = resolved;
|
|
868
|
+
while (true) {
|
|
869
|
+
try {
|
|
870
|
+
const real = fs.realpathSync(current);
|
|
871
|
+
return tail.length === 0 ? real : path.join(real, ...tail.reverse());
|
|
872
|
+
} catch (err) {
|
|
873
|
+
if (!isEnoent(err)) throw err;
|
|
874
|
+
const parent = path.dirname(current);
|
|
875
|
+
if (parent === current) return resolved;
|
|
876
|
+
tail.push(path.basename(current));
|
|
877
|
+
current = parent;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
856
882
|
function isSupportedSessionVersion(version: unknown): boolean {
|
|
857
883
|
return (
|
|
858
884
|
version === undefined ||
|
|
@@ -4239,6 +4265,46 @@ export class SessionManager {
|
|
|
4239
4265
|
}
|
|
4240
4266
|
}
|
|
4241
4267
|
|
|
4268
|
+
/**
|
|
4269
|
+
* Exact-delete a session transcript and its artifacts by path WITHOUT requiring
|
|
4270
|
+
* managed logical authorization. This is only for discarding an UNCOMMITTED
|
|
4271
|
+
* successor that a transaction (e.g. handoff) created via `newSession()` but
|
|
4272
|
+
* never durably authorized — such a session has no managed candidate listing
|
|
4273
|
+
* yet, so `dropSession` would refuse it and leak its resident-cache/artifact
|
|
4274
|
+
* root. Bounded to this manager's configured session root and refuses to touch
|
|
4275
|
+
* the active session; tolerates missing files.
|
|
4276
|
+
*/
|
|
4277
|
+
async discardUncommittedSession(sessionPath: string): Promise<void> {
|
|
4278
|
+
// Close the writer FIRST so the canonical checks and the delete run with no
|
|
4279
|
+
// intervening await, eliminating the check/use window in which a parent
|
|
4280
|
+
// directory could be swapped for a symlink between authorization and deletion.
|
|
4281
|
+
await this.#closePersistWriter();
|
|
4282
|
+
// Canonicalize the candidate (and the active file / configured dir) through
|
|
4283
|
+
// their nearest existing ancestor so symlink/reparse components cannot alias
|
|
4284
|
+
// the active session or escape the configured session directory, even when
|
|
4285
|
+
// the successor transcript itself does not yet exist.
|
|
4286
|
+
const canonicalCandidate = canonicalizeThroughExistingAncestor(path.resolve(sessionPath));
|
|
4287
|
+
if (this.#sessionFile) {
|
|
4288
|
+
const canonicalActive = canonicalizeThroughExistingAncestor(path.resolve(this.#sessionFile));
|
|
4289
|
+
if (canonicalActive === canonicalCandidate) {
|
|
4290
|
+
throw new Error("Refusing to discard the active session as uncommitted.");
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
// Least-authority containment: only within this manager's configured session
|
|
4294
|
+
// directory (canonical), where newSession() allocates the successor. The
|
|
4295
|
+
// broader managed root (parent of sessionDir) is deliberately NOT accepted.
|
|
4296
|
+
const canonicalSessionDir = canonicalizeThroughExistingAncestor(path.resolve(this.sessionDir));
|
|
4297
|
+
if (!pathIsWithin(canonicalSessionDir, canonicalCandidate)) {
|
|
4298
|
+
throw new Error("Uncommitted session discard is limited to this manager's configured session directory.");
|
|
4299
|
+
}
|
|
4300
|
+
try {
|
|
4301
|
+
await this.storage.deleteSessionWithArtifacts(canonicalCandidate);
|
|
4302
|
+
} catch (err) {
|
|
4303
|
+
if (isEnoent(err)) return;
|
|
4304
|
+
throw err;
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4242
4308
|
/**
|
|
4243
4309
|
* Fork the current session, creating a new session file with the same entries.
|
|
4244
4310
|
* Returns both the old and new session file paths for artifact copying.
|
|
@@ -107,6 +107,26 @@ export class YieldQueue {
|
|
|
107
107
|
this.#idleFlushPending = false;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/** Drop only the queued entries of a single kind, leaving other kinds intact. */
|
|
111
|
+
clearKind(kind: string): void {
|
|
112
|
+
this.#entries.delete(kind);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Re-schedule an idle flush if work remains and the session is idle. Used after
|
|
117
|
+
* a transition (e.g. handoff) releases a delivery fence so entries queued while
|
|
118
|
+
* fenced are not stranded until an unrelated enqueue or agent yield.
|
|
119
|
+
*/
|
|
120
|
+
rearmIdle(): void {
|
|
121
|
+
if (this.#options.isStreaming()) return;
|
|
122
|
+
for (const entries of this.#entries.values()) {
|
|
123
|
+
if (entries.length > 0) {
|
|
124
|
+
this.#scheduleIdleFlush();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
110
130
|
#scheduleIdleFlush(): void {
|
|
111
131
|
if (this.#idleFlushPending) return;
|
|
112
132
|
this.#idleFlushPending = true;
|
|
@@ -1435,6 +1435,42 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1435
1435
|
await runtime.ctx.handleCompactCommand(customInstructions);
|
|
1436
1436
|
},
|
|
1437
1437
|
},
|
|
1438
|
+
{
|
|
1439
|
+
name: "handoff",
|
|
1440
|
+
priority: 71,
|
|
1441
|
+
description: "Generate a handoff and continue in a new session",
|
|
1442
|
+
acpDescription: "Generate a handoff document and start a new session",
|
|
1443
|
+
inlineHint: "[focus instructions]",
|
|
1444
|
+
acpInputHint: "[focus instructions]",
|
|
1445
|
+
allowArgs: true,
|
|
1446
|
+
handle: async (command, runtime) => {
|
|
1447
|
+
let result: Awaited<ReturnType<typeof runtime.session.handoff>>;
|
|
1448
|
+
try {
|
|
1449
|
+
result = await runtime.session.handoff(command.args || undefined);
|
|
1450
|
+
} catch (err) {
|
|
1451
|
+
// Handoff precondition failures (nothing to hand off, streaming),
|
|
1452
|
+
// cancellation, and provider errors propagate as plain Errors; the
|
|
1453
|
+
// switch is non-destructive so the current session is unchanged.
|
|
1454
|
+
return usage(`Handoff failed: ${errorMessage(err)}; current session is unchanged.`, runtime);
|
|
1455
|
+
}
|
|
1456
|
+
if (!result) {
|
|
1457
|
+
return usage(
|
|
1458
|
+
"Handoff not created (cancelled or nothing to hand off); current session is unchanged.",
|
|
1459
|
+
runtime,
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
await runtime.output(
|
|
1463
|
+
result.savedPath
|
|
1464
|
+
? `Handoff created; new session started. Handoff document saved to: ${result.savedPath}`
|
|
1465
|
+
: "Handoff created; new session started with handoff context.",
|
|
1466
|
+
);
|
|
1467
|
+
return commandConsumed();
|
|
1468
|
+
},
|
|
1469
|
+
handleTui: async (command, runtime) => {
|
|
1470
|
+
runtime.ctx.editor.setText("");
|
|
1471
|
+
await runtime.ctx.handleHandoffCommand(command.args || undefined);
|
|
1472
|
+
},
|
|
1473
|
+
},
|
|
1438
1474
|
{
|
|
1439
1475
|
name: "contribute-pr",
|
|
1440
1476
|
aliases: ["contribution-prep"],
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* source; registering returns a disposer.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { logger } from "@gajae-code/utils";
|
|
10
11
|
import type { WorkflowGateEmitter } from "../modes/shared/agent-wire/workflow-gate-broker";
|
|
11
12
|
import type { AskAnswerSource } from "./index";
|
|
12
13
|
|
|
@@ -31,7 +32,20 @@ export function getAskAnswerSource(sessionId: string): AskAnswerSource | undefin
|
|
|
31
32
|
export function notifyWorkflowGateEmitterChanged(sessionId: string, emitter: WorkflowGateEmitter | undefined): void {
|
|
32
33
|
if (emitter) workflowGateEmitters.set(sessionId, emitter);
|
|
33
34
|
else workflowGateEmitters.delete(sessionId);
|
|
34
|
-
for (const listener of workflowGateListeners.get(sessionId) ?? [])
|
|
35
|
+
for (const listener of workflowGateListeners.get(sessionId) ?? []) {
|
|
36
|
+
// Isolate each listener: a throwing observer must never escape into an
|
|
37
|
+
// emitter suspend/bind/restore step, which runs inside session-transition
|
|
38
|
+
// transactions (e.g. handoff) whose rollback/commit correctness depends on
|
|
39
|
+
// this notification being no-throw.
|
|
40
|
+
try {
|
|
41
|
+
listener(emitter);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
logger.warn("Workflow-gate emitter listener threw during notification", {
|
|
44
|
+
sessionId,
|
|
45
|
+
error: error instanceof Error ? error.message : String(error),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
/** Observe workflow-gate emitter installation even when it occurs after session_start. */
|