mtech-ai-local-runner 0.1.105 → 0.1.106
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/README.md
CHANGED
|
@@ -354,11 +354,20 @@ apart. Codex jobs run with `features.hooks=false`, so they run no hook at all.
|
|
|
354
354
|
|
|
355
355
|
An ask is shown at a session start, at the next prompt (where it also counts as seen), at a turn end, or by waking an
|
|
356
356
|
idle Claude session through an `asyncRewake` Stop watcher. An ask shown at a session start, at a turn end or by a wake
|
|
357
|
-
counts as seen at the next prompt in that same session.
|
|
358
|
-
|
|
357
|
+
counts as seen at the next prompt in that same session. Ordinary and acceptance deliveries require a human prompt
|
|
358
|
+
within 30 minutes to wake a session. An exact investigation approval notice can wake its live bound Claude session
|
|
359
|
+
without a recent prompt, but still needs its own watcher and the 6-wakes-per-hour allowance. With no such session the ask becomes one macOS notification, which gives up after 3
|
|
359
360
|
seconds and logs only an error code, and the ask waits for the next session start or prompt. Hook text is fixed and
|
|
360
361
|
carries validated ids only; the AI reads the ask itself with the Gateway show tools, which mark its text as untrusted.
|
|
361
362
|
|
|
363
|
+
Unreleased source adds a Claude SessionStart `asyncRewake` watcher so a resumed conversation can receive that exact
|
|
364
|
+
investigation notice without a typed prompt. Synchronous SessionStart leaves those notices pending for the watcher,
|
|
365
|
+
regardless of hook arrival order. Neither registration nor the watcher invents a human prompt or a turn end; a busy
|
|
366
|
+
turn waits for its real Stop. Ordinary requests, acceptance delivery and Codex hooks retain their existing behavior.
|
|
367
|
+
Runner 0.1.105 does not include this fix. A separately reviewed package and installation of its updated Claude hooks
|
|
368
|
+
are required; package publication alone does not refresh an already open conversation. The recipient must still
|
|
369
|
+
check current Gateway authorization before accessing the pinned files or recording its result.
|
|
370
|
+
|
|
362
371
|
An ask or answer the runner puts into a session on its own, by a wake or at a turn end, also gets one macOS
|
|
363
372
|
notification naming that app and session ("It is in your Claude session in api-cloud-service-ai-gateway (last typed
|
|
364
373
|
17:39)."), since the person may be looking elsewhere (Work Item 643dc395). Hooks carry no session title, so the
|
|
@@ -154,7 +154,7 @@ function parseArguments(argv, env) {
|
|
|
154
154
|
return null;
|
|
155
155
|
}
|
|
156
156
|
const [app, event, ...extra] = rest;
|
|
157
|
-
if (extra.length > 0 || !isApp(app) || !isHookEvent(event))
|
|
157
|
+
if (extra.length > 0 || !isApp(app) || (!isHookEvent(event) && !(app === "claude" && event === "start-watch")))
|
|
158
158
|
return null;
|
|
159
159
|
return { stateRoot, app, event };
|
|
160
160
|
}
|
|
@@ -259,15 +259,18 @@ export async function runHookClient(argv, io) {
|
|
|
259
259
|
const input = record === null ? null : hookFields(record);
|
|
260
260
|
if (record === null || input === null)
|
|
261
261
|
return 0;
|
|
262
|
+
if (args.event === "start-watch" && record.hook_event_name !== "SessionStart")
|
|
263
|
+
return 0;
|
|
262
264
|
// Before the first byte goes to the hub: a session nobody attends is not registered, shown an ask or counted as seen.
|
|
263
265
|
const lineage = args.app === "claude" ? (io.processLineage ?? readProcessLineage)() : null;
|
|
264
266
|
if (!isHumanSession(args.app, io.env, record, args.stateRoot, lineage))
|
|
265
267
|
return 0;
|
|
266
268
|
const socketPath = join(args.stateRoot, HUB_SOCKET);
|
|
267
269
|
const hubTimeoutMs = io.hubTimeoutMs ?? HUB_TIMEOUT_MS;
|
|
268
|
-
if (args.app === "claude" && args.event === "stop") {
|
|
270
|
+
if (args.app === "claude" && (args.event === "stop" || args.event === "start-watch")) {
|
|
269
271
|
// The asyncRewake watcher: exit 2 with the text on stderr wakes Claude, even when the session is idle.
|
|
270
|
-
const text = await converse(socketPath, encodeLine({ v: 1, op: "watch", app: "claude", session_id: input.sessionId, cwd: input.cwd
|
|
272
|
+
const text = await converse(socketPath, encodeLine({ v: 1, op: "watch", app: "claude", session_id: input.sessionId, cwd: input.cwd,
|
|
273
|
+
...(args.event === "start-watch" ? { source: "session-start" } : {}) }), hubTimeoutMs, io.watchLimitMs ?? WATCH_LIMIT_MS, (line, first) => {
|
|
271
274
|
const message = parseWatchMessage(line);
|
|
272
275
|
if (message?.type === "wake")
|
|
273
276
|
return { done: true, value: message.text };
|
|
@@ -10,6 +10,7 @@ import { MAX_CWD_LENGTH, SESSION_ID, isApp, isHookEvent } from "./types.js";
|
|
|
10
10
|
* (a client from before the flag sends no "human"; that prompt counts as typed)
|
|
11
11
|
* {"v":1,"op":"event","app":"codex","event":"stop","session_id":…,"cwd":…,"stop_hook_active":false}
|
|
12
12
|
* {"v":1,"op":"watch","app":"claude","session_id":…,"cwd":…} (the Claude Stop watcher)
|
|
13
|
+
* {"v":1,"op":"watch","app":"claude","session_id":…,"cwd":…,"source":"session-start"} (resume watcher)
|
|
13
14
|
* {"v":1,"op":"status"} (doctor)
|
|
14
15
|
* Hub to client:
|
|
15
16
|
* event: {"v":1,"ok":true,"text":"…"|null}, then the hub closes the connection
|
|
@@ -58,9 +59,11 @@ export function parseHubRequest(line) {
|
|
|
58
59
|
if (typeof value.cwd !== "string" || value.cwd.length > MAX_CWD_LENGTH)
|
|
59
60
|
return null;
|
|
60
61
|
if (value.op === "watch") {
|
|
61
|
-
if (value.app !== "claude" || Object.keys(value).length !== 5)
|
|
62
|
+
if (value.app !== "claude" || Object.keys(value).length !== (value.source === undefined ? 5 : 6)
|
|
63
|
+
|| (value.source !== undefined && value.source !== "session-start"))
|
|
62
64
|
return null;
|
|
63
|
-
return { v: 1, op: "watch", app: "claude", session_id: value.session_id, cwd: value.cwd
|
|
65
|
+
return { v: 1, op: "watch", app: "claude", session_id: value.session_id, cwd: value.cwd,
|
|
66
|
+
...(value.source === "session-start" ? { source: "session-start" } : {}) };
|
|
64
67
|
}
|
|
65
68
|
if (value.op !== "event" || !isApp(value.app) || !isHookEvent(value.event))
|
|
66
69
|
return null;
|
package/dist/src/asks/inbox.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { emptyAskState, pruneAskState, sessionKey } from "./store.js";
|
|
2
|
-
import { addDeliveries, hashSessionId,
|
|
2
|
+
import { addDeliveries, hashSessionId, claimOpen, claimShownNotifications, closeDeliveries, confirmNotified, confirmOpened, confirmWake, isLive, openApp, planRoutes, preferredApp, promptSubmitted, releaseOpen, sessionEnded, sessionStarted, turnEnded, } from "./routing.js";
|
|
3
3
|
import { MAX_TARGET_HASHES, droppableDeliveryId, parseAskDelivery } from "./types.js";
|
|
4
4
|
import { renderOutput } from "./templates.js";
|
|
5
5
|
import { PRESENCE_RECHECK_MS } from "./presence.js";
|
|
@@ -77,11 +77,14 @@ export class AskInbox {
|
|
|
77
77
|
const session = key === null ? undefined : this.state.sessions[key];
|
|
78
78
|
return session === undefined ? undefined : sessionPlace(session.cwd, session.lastPromptAt);
|
|
79
79
|
}
|
|
80
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Readiness names live attended sessions. The Gateway checks signed approval and exact identity;
|
|
82
|
+
* ordinary and acceptance delivery still apply their human-prompt age rule when routing locally.
|
|
83
|
+
*/
|
|
81
84
|
acceptanceSessionHashes() {
|
|
82
85
|
const now = this.now();
|
|
83
86
|
return Object.values(this.state.sessions)
|
|
84
|
-
.filter(session => isLive(session, now)
|
|
87
|
+
.filter(session => isLive(session, now))
|
|
85
88
|
.sort((left, right) => right.lastEventAt - left.lastEventAt)
|
|
86
89
|
.slice(0, MAX_TARGET_HASHES)
|
|
87
90
|
.map(session => hashSessionId(session.sessionId));
|
|
@@ -120,9 +123,10 @@ export class AskInbox {
|
|
|
120
123
|
});
|
|
121
124
|
}
|
|
122
125
|
/**
|
|
123
|
-
* A Claude Stop watcher. A
|
|
126
|
+
* A Claude Stop or SessionStart watcher. A real turn end runs
|
|
124
127
|
* turnEnded(), which shows what is already pinned to the session (via stop) and marks the session idle. Without that
|
|
125
128
|
* the session would still count as in a turn, and the wake branch of the routing rules could never be reached.
|
|
129
|
+
* A SessionStart watcher only waits; it cannot mark an interrupted or running turn ended.
|
|
126
130
|
* What is not pinned makes the watcher wait, replacing any older watcher of the same session.
|
|
127
131
|
*/
|
|
128
132
|
watch(request, watcher) {
|
|
@@ -133,7 +137,8 @@ export class AskInbox {
|
|
|
133
137
|
const key = sessionKey("claude", request.session_id);
|
|
134
138
|
this.release(key, "replaced");
|
|
135
139
|
// Claude's own loop guard is the hourly wake limit; stop_hook_active belongs to Stop decisions, not to a rewake.
|
|
136
|
-
const text =
|
|
140
|
+
const text = request.source === "session-start" ? null
|
|
141
|
+
: renderOutput(turnEnded(this.state, { app: "claude", sessionId: request.session_id, cwd: request.cwd, now: this.now() }, { stopHookActive: false }));
|
|
137
142
|
if (text !== null) {
|
|
138
143
|
watcher.wake(text);
|
|
139
144
|
}
|
|
@@ -67,7 +67,7 @@ export function claudePluginManifest() {
|
|
|
67
67
|
export function claudeHooksFile(stateRoot) {
|
|
68
68
|
const handler = (event, extra = {}) => ({ type: "command", command: hookCommand(stateRoot, "claude", event), ...extra });
|
|
69
69
|
return { description: CLAUDE_HOOKS_DESCRIPTION, hooks: {
|
|
70
|
-
SessionStart: [{ hooks: [handler("session-start", { timeout: 10 })] }],
|
|
70
|
+
SessionStart: [{ hooks: [handler("session-start", { timeout: 10 }), handler("start-watch", { asyncRewake: true, timeout: 3600 })] }],
|
|
71
71
|
UserPromptSubmit: [{ hooks: [handler("prompt", { timeout: 10 })] }],
|
|
72
72
|
Stop: [{ hooks: [handler("stop", { asyncRewake: true, timeout: 3600 })] }],
|
|
73
73
|
SessionEnd: [{ hooks: [handler("session-end")] }],
|
package/dist/src/asks/routing.js
CHANGED
|
@@ -7,7 +7,7 @@ import { LOWER_UUID } from "./types.js";
|
|
|
7
7
|
* Every function here works on the in-memory AskState and does no I/O, so the rules are tested directly.
|
|
8
8
|
* The AI's own actions never count as seen: only hook events the runner observes move a delivery forward.
|
|
9
9
|
*/
|
|
10
|
-
/**
|
|
10
|
+
/** Ordinary and acceptance deliveries require a human prompt within the last 30 minutes. */
|
|
11
11
|
export const WAKE_WINDOW_MS = 30 * 60 * 1000;
|
|
12
12
|
/** At most 6 wakes per session per hour (Claude asyncRewake exits and Codex Stop continuations). */
|
|
13
13
|
export const WAKE_LIMIT = 6;
|
|
@@ -123,6 +123,8 @@ export function chooseSession(item, state, now) {
|
|
|
123
123
|
* becomes the chosen one by that prompt (see promptSubmitted);
|
|
124
124
|
* - an idle Claude session whose Stop watcher waits, under its hourly limit: woken now;
|
|
125
125
|
* - otherwise (an idle Codex session, no watcher, or the limit reached): a notification.
|
|
126
|
+
* An exact investigation notice already names a signed approved scope, so its own live session can
|
|
127
|
+
* receive it without a recent human prompt. Its watcher and hourly wake limit are still required.
|
|
126
128
|
* An answer whose asking session is live is pinned or woken there by the same rules, and otherwise held there: it waits
|
|
127
129
|
* for that session, with a notification that says so, and no session is opened for it (Work Decision 8562d191).
|
|
128
130
|
*/
|
|
@@ -132,11 +134,12 @@ export function routeItem(item, state, now, watching) {
|
|
|
132
134
|
const session = key === null ? undefined : state.sessions[key];
|
|
133
135
|
if (key === null || session === undefined)
|
|
134
136
|
return { action: "notify" };
|
|
135
|
-
|
|
137
|
+
const approvedInvestigation = item.subjectType === "investigation" && item.routingPolicy === "exact";
|
|
138
|
+
if (asking === null && !approvedInvestigation && !humanPromptWithin(session, now))
|
|
136
139
|
return { action: "notify" };
|
|
137
140
|
if (turnInProgress(session))
|
|
138
141
|
return { action: "pin", sessionKey: key };
|
|
139
|
-
if (session.app === "claude" && humanPromptWithin(session, now) && watching.has(key) && wakesLeft(session, now) > 0)
|
|
142
|
+
if (session.app === "claude" && (approvedInvestigation || humanPromptWithin(session, now)) && watching.has(key) && wakesLeft(session, now) > 0)
|
|
140
143
|
return { action: "wake", sessionKey: key };
|
|
141
144
|
return asking === null ? { action: "notify" } : { action: "hold", sessionKey: key };
|
|
142
145
|
}
|
|
@@ -225,7 +228,10 @@ export function closeDeliveries(state, deliveryIds) {
|
|
|
225
228
|
/** SessionStart: register the session and show what waits for any session (seen at its next prompt). */
|
|
226
229
|
export function sessionStarted(state, context) {
|
|
227
230
|
const { key } = touch(state, context);
|
|
228
|
-
|
|
231
|
+
// SessionStart adds context but does not itself start a model turn. Leave exact Claude
|
|
232
|
+
// investigations for the asynchronous watcher, regardless of which startup hook arrives first.
|
|
233
|
+
const output = selectForOutput(dueFor(state, key, context.now).filter(item => !(context.app === "claude"
|
|
234
|
+
&& item.subjectType === "investigation" && item.routingPolicy === "exact")));
|
|
229
235
|
for (const item of everyItem(output))
|
|
230
236
|
markShown(item, key, context.app, "session_start", context.now);
|
|
231
237
|
return output;
|