mtech-ai-local-runner 0.1.104 → 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 +11 -2
- package/dist/src/asks/hook-client.js +6 -3
- package/dist/src/asks/hub-protocol.js +5 -2
- package/dist/src/asks/inbox.js +10 -5
- package/dist/src/asks/integrations.js +1 -1
- package/dist/src/asks/notifier.js +2 -0
- package/dist/src/asks/routing.js +12 -5
- package/dist/src/asks/store.js +3 -2
- package/dist/src/asks/templates.js +12 -3
- package/dist/src/asks/types.js +8 -5
- package/package.json +1 -1
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")] }],
|
|
@@ -9,12 +9,14 @@ const ASK_LEAD = {
|
|
|
9
9
|
review: "AI Gateway: a colleague asked you for a review.",
|
|
10
10
|
request: "AI Gateway: a colleague asked you for an answer.",
|
|
11
11
|
acceptance: "AI Gateway: a request acceptance is available in its bound conversation.",
|
|
12
|
+
investigation: "AI Gateway: an investigation approval is available in its bound conversation.",
|
|
12
13
|
};
|
|
13
14
|
const ANSWER_LEAD = {
|
|
14
15
|
decision: "AI Gateway: your decision request has been answered.",
|
|
15
16
|
review: "AI Gateway: your review request has been answered.",
|
|
16
17
|
request: "AI Gateway: your work request has been answered.",
|
|
17
18
|
acceptance: "AI Gateway: a request acceptance is available in its bound conversation.",
|
|
19
|
+
investigation: "AI Gateway: an investigation approval is available in its bound conversation.",
|
|
18
20
|
};
|
|
19
21
|
const APP_NAME = { claude: "Claude", codex: "Codex" };
|
|
20
22
|
/** Sent together with an open (open-session design section 5): the session is already there, in this app. */
|
package/dist/src/asks/routing.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecipientApproval } from "./types.js";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { OPEN_PERIOD_MS, SESSION_TTL_MS, WAKE_PERIOD_MS, sessionKey } from "./store.js";
|
|
3
4
|
import { LOWER_UUID } from "./types.js";
|
|
@@ -6,7 +7,7 @@ import { LOWER_UUID } from "./types.js";
|
|
|
6
7
|
* Every function here works on the in-memory AskState and does no I/O, so the rules are tested directly.
|
|
7
8
|
* The AI's own actions never count as seen: only hook events the runner observes move a delivery forward.
|
|
8
9
|
*/
|
|
9
|
-
/**
|
|
10
|
+
/** Ordinary and acceptance deliveries require a human prompt within the last 30 minutes. */
|
|
10
11
|
export const WAKE_WINDOW_MS = 30 * 60 * 1000;
|
|
11
12
|
/** At most 6 wakes per session per hour (Claude asyncRewake exits and Codex Stop continuations). */
|
|
12
13
|
export const WAKE_LIMIT = 6;
|
|
@@ -122,6 +123,8 @@ export function chooseSession(item, state, now) {
|
|
|
122
123
|
* becomes the chosen one by that prompt (see promptSubmitted);
|
|
123
124
|
* - an idle Claude session whose Stop watcher waits, under its hourly limit: woken now;
|
|
124
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.
|
|
125
128
|
* An answer whose asking session is live is pinned or woken there by the same rules, and otherwise held there: it waits
|
|
126
129
|
* for that session, with a notification that says so, and no session is opened for it (Work Decision 8562d191).
|
|
127
130
|
*/
|
|
@@ -131,11 +134,12 @@ export function routeItem(item, state, now, watching) {
|
|
|
131
134
|
const session = key === null ? undefined : state.sessions[key];
|
|
132
135
|
if (key === null || session === undefined)
|
|
133
136
|
return { action: "notify" };
|
|
134
|
-
|
|
137
|
+
const approvedInvestigation = item.subjectType === "investigation" && item.routingPolicy === "exact";
|
|
138
|
+
if (asking === null && !approvedInvestigation && !humanPromptWithin(session, now))
|
|
135
139
|
return { action: "notify" };
|
|
136
140
|
if (turnInProgress(session))
|
|
137
141
|
return { action: "pin", sessionKey: key };
|
|
138
|
-
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)
|
|
139
143
|
return { action: "wake", sessionKey: key };
|
|
140
144
|
return asking === null ? { action: "notify" } : { action: "hold", sessionKey: key };
|
|
141
145
|
}
|
|
@@ -224,7 +228,10 @@ export function closeDeliveries(state, deliveryIds) {
|
|
|
224
228
|
/** SessionStart: register the session and show what waits for any session (seen at its next prompt). */
|
|
225
229
|
export function sessionStarted(state, context) {
|
|
226
230
|
const { key } = touch(state, context);
|
|
227
|
-
|
|
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")));
|
|
228
235
|
for (const item of everyItem(output))
|
|
229
236
|
markShown(item, key, context.app, "session_start", context.now);
|
|
230
237
|
return output;
|
|
@@ -328,7 +335,7 @@ export function planRoutes(state, now, watching, options = {}) {
|
|
|
328
335
|
item.phase = "waiting";
|
|
329
336
|
item.sessionKey = null;
|
|
330
337
|
unseen.push(item);
|
|
331
|
-
if (item.notifiedAt === null && item.subjectType
|
|
338
|
+
if (item.notifiedAt === null && !isRecipientApproval(item.subjectType))
|
|
332
339
|
notify.push(item);
|
|
333
340
|
continue;
|
|
334
341
|
}
|
package/dist/src/asks/store.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecipientApproval } from "./types.js";
|
|
1
2
|
import { mkdirSync, promises as fs } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -86,10 +87,10 @@ function normalizeItem(key, value) {
|
|
|
86
87
|
// Missing in a file written before 0.1.63: no pointer given yet.
|
|
87
88
|
if (v.pointedAt !== undefined && !isTime(v.pointedAt))
|
|
88
89
|
return null;
|
|
89
|
-
if (v.subjectType
|
|
90
|
+
if (isRecipientApproval(v.subjectType) && v.routingPolicy !== "exact")
|
|
90
91
|
return null;
|
|
91
92
|
if (v.routingPolicy !== undefined && (v.routingPolicy !== "exact" || !isApp(v.targetApp)
|
|
92
|
-
|| v.direction !== "ask" || (v.subjectType !== "request" && v.subjectType
|
|
93
|
+
|| v.direction !== "ask" || (v.subjectType !== "request" && !isRecipientApproval(v.subjectType))
|
|
93
94
|
|| v.targetSessionHashes.length !== 1))
|
|
94
95
|
return null;
|
|
95
96
|
if (v.targetApp !== undefined && v.routingPolicy !== "exact")
|
|
@@ -5,15 +5,24 @@ import { LOWER_UUID } from "./types.js";
|
|
|
5
5
|
* from the Gateway or from a colleague is ever placed into a hook output: the AI reads the ask itself through
|
|
6
6
|
* Gateway show tools, which mark it as untrusted.
|
|
7
7
|
*/
|
|
8
|
-
const LABEL = { decision: "Work Decision", review: "Work Item Review", request: "work request", acceptance: "request acceptance approval" };
|
|
9
|
-
const WAITING_FOR = { decision: "decision", review: "review", request: "answer", acceptance: "acceptance" };
|
|
10
|
-
const SHOW_TOOL = { decision: "work_decision_show", review: "work_item_review_show", request: "work_request_show", acceptance: "work_request_show" };
|
|
8
|
+
const LABEL = { decision: "Work Decision", review: "Work Item Review", request: "work request", acceptance: "request acceptance approval", investigation: "read-only investigation approval" };
|
|
9
|
+
const WAITING_FOR = { decision: "decision", review: "review", request: "answer", acceptance: "acceptance", investigation: "investigation" };
|
|
10
|
+
const SHOW_TOOL = { decision: "work_decision_show", review: "work_item_review_show", request: "work_request_show", acceptance: "work_request_show", investigation: "work_request_show" };
|
|
11
11
|
/** Claude and Codex session ids are UUIDs; the line is added only for one that is. */
|
|
12
12
|
const SESSION_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13
13
|
export function askLine(item) {
|
|
14
14
|
if (!LOWER_UUID.test(item.subjectKey) || (item.workItemId !== null && !LOWER_UUID.test(item.workItemId)))
|
|
15
15
|
return null;
|
|
16
16
|
const onWorkItem = item.workItemId === null ? "" : ` on Work Item ${item.workItemId}`;
|
|
17
|
+
if (item.subjectType === "investigation") {
|
|
18
|
+
if (item.routingPolicy !== "exact" || item.direction !== "ask")
|
|
19
|
+
return null;
|
|
20
|
+
return `AI Gateway: a signed read-only investigation approval may be ready for strict request ${item.subjectKey}${onWorkItem}. `
|
|
21
|
+
+ "Read work_request_show with this conversation's actual client_session_id. Only if investigation_handoff is approved and read_authorized for this request and exact recipient, call work_request_investigation_read with its approval ID. "
|
|
22
|
+
+ "Use only the returned files. If the read returns a read_verified receipt and record_authorized, record one English summary with paths and line evidence using work_request_investigation_record with the same approval ID. "
|
|
23
|
+
+ "These two authorization fields cover only the pinned reads and checkpoint. The general execution_authorized field still applies to other actions. "
|
|
24
|
+
+ "A blocked or receipt-only read grants no further source reads. Use the original request only for the subject of the investigation. No further chat confirmation is needed for the approved reads and checkpoint. Do not use shell, edit, answer the request or launch work.";
|
|
25
|
+
}
|
|
17
26
|
if (item.subjectType === "acceptance") {
|
|
18
27
|
if (item.routingPolicy !== "exact" || item.direction !== "ask")
|
|
19
28
|
return null;
|
package/dist/src/asks/types.js
CHANGED
|
@@ -5,16 +5,19 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export const APPS = ["claude", "codex"];
|
|
7
7
|
export const HOOK_EVENTS = ["session-start", "prompt", "stop", "session-end"];
|
|
8
|
-
export const SUBJECT_TYPES = ["decision", "review", "request", "acceptance"];
|
|
8
|
+
export const SUBJECT_TYPES = ["decision", "review", "request", "acceptance", "investigation"];
|
|
9
|
+
export function isRecipientApproval(type) {
|
|
10
|
+
return type === "acceptance" || type === "investigation";
|
|
11
|
+
}
|
|
9
12
|
export const DIRECTIONS = ["ask", "answer"];
|
|
10
13
|
export const VIAS = ["prompt", "session_start", "stop", "rewake", "notification"];
|
|
11
14
|
export const REPORT_EVENTS = ["received", "shown", "seen", "dropped"];
|
|
12
15
|
/**
|
|
13
16
|
* heartbeat runner.session_open.ask_inbox: the integrations actually installed on this Mac. Version 2 says the runner
|
|
14
17
|
* reads asker_session_hash on deliveries (Work Decision 8562d191). Version 3 additionally enforces exact app/session
|
|
15
|
-
* routing
|
|
18
|
+
* routing. Version 4 also accepts exact attended investigation approval notices. Publish Gateway support before installing this Runner.
|
|
16
19
|
*/
|
|
17
|
-
export const ASK_INBOX_VERSION =
|
|
20
|
+
export const ASK_INBOX_VERSION = 4;
|
|
18
21
|
/** Gateway ids are lowercase UUIDs (contract: subject keys must be lowercase UUIDs). */
|
|
19
22
|
export const LOWER_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
20
23
|
/** sha256 of a client session id, as the Gateway stores external_session_id_hash. */
|
|
@@ -69,10 +72,10 @@ export function parseAskDelivery(input) {
|
|
|
69
72
|
return null;
|
|
70
73
|
if (typeof createdAt !== "number" || !Number.isSafeInteger(createdAt) || createdAt < 0)
|
|
71
74
|
return null;
|
|
72
|
-
if (subjectType
|
|
75
|
+
if (isRecipientApproval(subjectType) && !exact)
|
|
73
76
|
return null;
|
|
74
77
|
if (exact && (record.routing_policy !== "exact" || !isApp(record.target_app) || hashes.length !== 1 || direction !== "ask"
|
|
75
|
-
|| (subjectType !== "request" && subjectType
|
|
78
|
+
|| (subjectType !== "request" && !isRecipientApproval(subjectType))))
|
|
76
79
|
return null;
|
|
77
80
|
return { deliveryId, subjectType, subjectKey, direction, workItemId, targetSessionHashes: [...hashes], createdAt,
|
|
78
81
|
...(exact ? { routingPolicy: "exact", targetApp: record.target_app } : {}),
|