akm-cli 0.10.26101201-alpha → 0.10.26101202-alpha
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
CHANGED
|
@@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.10.26101202] - 2026-10-12
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **A signal to `akm agent` no longer leaves the CLI harness running (#1049).** `akm agent` on the `opencode`,
|
|
14
|
+
`claude` and `codex` CLI engines runs the harness in its own process group, so SIGTERM or SIGINT to akm ended akm and
|
|
15
|
+
left `opencode run` (and, on OpenCode 2, its `serve --stdio` child) running. The managed-subprocess primitive now runs
|
|
16
|
+
the timeout kill ladder on every live group child when akm gets such a signal with no handler of its own, then
|
|
17
|
+
re-raises it so akm still ends by the signal.
|
|
18
|
+
- **An OpenCode 2 SDK dispatch ends when akm rejects a permission request (#1049).** OpenCode 2 interrupts the step
|
|
19
|
+
(`aborted`, "Step interrupted") and goes idle without writing the closing `idle` message, so the dispatch waited out
|
|
20
|
+
its whole deadline. It now returns at once with `reason: "aborted"` and the declined tool call in `error`.
|
|
21
|
+
- **Documented: OpenCode permission defaults in a dispatch (#1049).** OpenCode 1 asks before reading or writing outside
|
|
22
|
+
the project directory; OpenCode 2 does not unless its config says `ask`, so akm's permission rejection only happens
|
|
23
|
+
on OpenCode 2 once you add that rule. akm changes no default. See [Configuration](docs/reference/configuration.md#engines).
|
|
24
|
+
|
|
9
25
|
## [0.10.26101201] - 2026-10-12
|
|
10
26
|
|
|
11
27
|
### Removed
|
package/dist/core/subprocess.js
CHANGED
|
@@ -116,6 +116,51 @@ export function scheduleKillLadder(proc, opts) {
|
|
|
116
116
|
if (typeof sigkillTimer !== "number")
|
|
117
117
|
sigkillTimer.unref?.();
|
|
118
118
|
}
|
|
119
|
+
const liveGroupChildren = new Set();
|
|
120
|
+
let signalGuardHandlers;
|
|
121
|
+
function removeSignalGuard() {
|
|
122
|
+
for (const [signal, handler] of signalGuardHandlers ?? [])
|
|
123
|
+
process.off(signal, handler);
|
|
124
|
+
signalGuardHandlers = undefined;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A captured child runs in its own process group, so the terminal's Ctrl-C and
|
|
128
|
+
* a `kill <akm-pid>` no longer reach it: without this, SIGTERM/SIGINT to akm
|
|
129
|
+
* ended akm and left the harness CLI (and everything it started) running. Each
|
|
130
|
+
* live child is registered here and, on the first such signal, takes the same
|
|
131
|
+
* ladder as a timeout; once every child is gone the signal is re-raised so akm
|
|
132
|
+
* still ends by the signal itself.
|
|
133
|
+
*
|
|
134
|
+
* The listeners are installed only while a child is live and only when nothing
|
|
135
|
+
* else handles the signal. A command that handles it itself (`workflow run`,
|
|
136
|
+
* `improve`, task runs) unwinds through its own cleanup, which already aborts
|
|
137
|
+
* its children, and must not have its handling pre-empted by a re-raise.
|
|
138
|
+
*/
|
|
139
|
+
function registerLiveGroupChild(child) {
|
|
140
|
+
liveGroupChildren.add(child);
|
|
141
|
+
if (!signalGuardHandlers && process.listenerCount("SIGINT") === 0 && process.listenerCount("SIGTERM") === 0) {
|
|
142
|
+
const handlers = [];
|
|
143
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
144
|
+
const handler = () => {
|
|
145
|
+
const children = [...liveGroupChildren];
|
|
146
|
+
for (const live of children)
|
|
147
|
+
live.terminate();
|
|
148
|
+
removeSignalGuard();
|
|
149
|
+
void Promise.allSettled(children.map((live) => live.proc.exited)).then(() => {
|
|
150
|
+
process.kill(process.pid, signal);
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
process.once(signal, handler);
|
|
154
|
+
handlers.push([signal, handler]);
|
|
155
|
+
}
|
|
156
|
+
signalGuardHandlers = handlers;
|
|
157
|
+
}
|
|
158
|
+
return () => {
|
|
159
|
+
liveGroupChildren.delete(child);
|
|
160
|
+
if (liveGroupChildren.size === 0)
|
|
161
|
+
removeSignalGuard();
|
|
162
|
+
};
|
|
163
|
+
}
|
|
119
164
|
/**
|
|
120
165
|
* The joined, human-readable reason ONE managed run's capture is incomplete, or
|
|
121
166
|
* `undefined` when both pipes drained cleanly.
|
|
@@ -401,6 +446,8 @@ export async function runManagedSubprocess(cmd, opts) {
|
|
|
401
446
|
else
|
|
402
447
|
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
403
448
|
}
|
|
449
|
+
// A termination signal to this process takes the own-group child down too.
|
|
450
|
+
const unregisterLiveChild = capture && spawnsOwnProcessGroup() ? registerLiveGroupChild({ proc, terminate: onAbort }) : () => { };
|
|
404
451
|
// Stream-drain timeout: a short grace once nothing living owns the pipe any
|
|
405
452
|
// more, or a one-hour safety bound when execution itself is unbounded.
|
|
406
453
|
//
|
|
@@ -454,6 +501,7 @@ export async function runManagedSubprocess(cmd, opts) {
|
|
|
454
501
|
if (timer !== undefined)
|
|
455
502
|
clearTimeoutImpl(timer);
|
|
456
503
|
abortSignal?.removeEventListener("abort", onAbort);
|
|
504
|
+
unregisterLiveChild();
|
|
457
505
|
// Drain readers so they don't surface as unhandled rejections. The streams
|
|
458
506
|
// carry their own drain timeout so this cannot block indefinitely.
|
|
459
507
|
await Promise.allSettled([stdoutPromise, stderrPromise]);
|
|
@@ -470,6 +518,7 @@ export async function runManagedSubprocess(cmd, opts) {
|
|
|
470
518
|
}
|
|
471
519
|
clearTimeoutImpl(timer);
|
|
472
520
|
abortSignal?.removeEventListener("abort", onAbort);
|
|
521
|
+
unregisterLiveChild();
|
|
473
522
|
const [stdoutRead, stderrRead] = await Promise.all([stdoutPromise, stderrPromise]);
|
|
474
523
|
return {
|
|
475
524
|
exitCode,
|
|
@@ -57,6 +57,27 @@ export function v2Turn(messages, userId) {
|
|
|
57
57
|
}
|
|
58
58
|
return undefined;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The turn that began with the user message `userId`, when OpenCode 2 ended it
|
|
62
|
+
* by interrupting a step instead of writing the closing `idle` message. Only
|
|
63
|
+
* meaningful once `session.wait` has returned (the session is not running).
|
|
64
|
+
*
|
|
65
|
+
* Recorded from 2.0.26: when akm rejects a permission request the step fails
|
|
66
|
+
* with `{ type: "aborted", message: "Step interrupted" }` (`finish: "error"`),
|
|
67
|
+
* the tool call is `error` ("The user declined this tool call"),
|
|
68
|
+
* `session.execution.interrupted` fires and the session goes idle, but no
|
|
69
|
+
* `idle` message is written and `session.idle` is not emitted.
|
|
70
|
+
*/
|
|
71
|
+
export function v2InterruptedTurn(messages, userId) {
|
|
72
|
+
const start = messages.findIndex((m) => m.id === userId);
|
|
73
|
+
if (start < 0)
|
|
74
|
+
return undefined;
|
|
75
|
+
const assistants = messages.slice(start + 1).filter((m) => m.type === "assistant");
|
|
76
|
+
const last = assistants.at(-1);
|
|
77
|
+
return last?.finish === "error" && last.error?.type === "aborted"
|
|
78
|
+
? { outcome: "interrupted", assistants }
|
|
79
|
+
: undefined;
|
|
80
|
+
}
|
|
60
81
|
/** Decode a finished turn into the answer, the usage of every step and the failure, if any. */
|
|
61
82
|
function v2PromptResult(turn) {
|
|
62
83
|
const text = turn.assistants
|
|
@@ -82,7 +103,11 @@ function v2PromptResult(turn) {
|
|
|
82
103
|
}
|
|
83
104
|
function v2Failure(turn) {
|
|
84
105
|
if (turn.outcome === "interrupted") {
|
|
85
|
-
|
|
106
|
+
// Say why when the last step says so, e.g. "Step interrupted: The user declined this tool call".
|
|
107
|
+
const last = turn.assistants.at(-1);
|
|
108
|
+
const declined = last?.content.flatMap((c) => c.type === "tool" && c.state.status === "error" ? [c.state.error.message] : []);
|
|
109
|
+
const detail = [last?.error?.message, ...(declined ?? [])].filter(Boolean).join(": ");
|
|
110
|
+
return { reason: "aborted", message: `OpenCode session was interrupted${detail ? ` (${detail})` : ""}` };
|
|
86
111
|
}
|
|
87
112
|
const last = turn.assistants.at(-1);
|
|
88
113
|
if (turn.outcome === "failed") {
|
|
@@ -154,7 +179,8 @@ export const V2_ADAPTER = {
|
|
|
154
179
|
const user = await c.session.prompt({ sessionID: sessionId, text }, { signal });
|
|
155
180
|
for (;;) {
|
|
156
181
|
await c.session.wait({ sessionID: sessionId }, { signal });
|
|
157
|
-
const
|
|
182
|
+
const messages = await c.session.context({ sessionID: sessionId }, { signal });
|
|
183
|
+
const turn = v2Turn(messages, user.id) ?? v2InterruptedTurn(messages, user.id);
|
|
158
184
|
if (turn)
|
|
159
185
|
return v2PromptResult(turn);
|
|
160
186
|
// `wait` can return before the queued prompt starts; look again.
|
|
@@ -168,6 +168,23 @@ working and logs a warning recommending OpenCode 2. A `--version` akm cannot rea
|
|
|
168
168
|
with the raw output. To run a particular OpenCode, set the engine's `bin`, e.g.
|
|
169
169
|
`{"kind":"agent","platform":"opencode-sdk","bin":"/opt/opencode1/bin/opencode"}`.
|
|
170
170
|
|
|
171
|
+
**Permissions in a dispatch.** akm answers every OpenCode permission request of a
|
|
172
|
+
dispatch with "reject" (there is no one to approve it), and the rejection reaches
|
|
173
|
+
the model as a tool error. akm changes no OpenCode permission default, so what
|
|
174
|
+
gets asked is OpenCode's own setting:
|
|
175
|
+
|
|
176
|
+
- **OpenCode 1** asks before reading or writing outside the project directory
|
|
177
|
+
(`external_directory`), so akm rejects it.
|
|
178
|
+
- **OpenCode 2** (2.0.26) does not ask: a read or write outside the project
|
|
179
|
+
directory just runs, and akm's rejection never happens, unless your OpenCode
|
|
180
|
+
config says to ask. Add a rule to the global `opencode.json`, e.g.
|
|
181
|
+
`"permissions": [{"action": "external_directory", "resource": "*", "effect": "ask"}]`
|
|
182
|
+
(`"read"` and `"shell"` are actions too).
|
|
183
|
+
|
|
184
|
+
On OpenCode 2 a rejection ends the turn: the dispatch returns `ok: false` with
|
|
185
|
+
`reason: "aborted"` and the declined tool call in `error`. On OpenCode 1 the model
|
|
186
|
+
carries on.
|
|
187
|
+
|
|
171
188
|
On an engine without `timeoutMs`, model work (an improve process, a quality or
|
|
172
189
|
triage judge, or an index pass) stops after 600 seconds, whatever the engine's
|
|
173
190
|
kind; other work on an agent engine runs until it finishes. An improve stage's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.26101202-alpha",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cursor.",
|
|
6
6
|
"keywords": [
|