@adhdev/mesh-shared 1.0.82-rc.3 → 1.0.82-rc.30
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/assistant-protocol.d.ts +140 -6
- package/dist/index.d.ts +6 -0
- package/dist/index.js +529 -110
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +500 -110
- package/dist/index.mjs.map +1 -1
- package/dist/launch-defaults.d.ts +194 -0
- package/dist/mesh-tool-names.d.ts +2 -2
- package/dist/mesh-vocabulary.d.ts +13 -4
- package/dist/node-facts.d.ts +41 -1
- package/dist/outbound-message.d.ts +7 -0
- package/dist/path-ownership.d.ts +1 -1
- package/dist/protocol/daemon-server.d.ts +9 -0
- package/dist/protocol/dashboard-daemon-p2p.d.ts +1 -1
- package/dist/protocol/dashboard-server.d.ts +10 -0
- package/dist/provider-approval.d.ts +36 -0
- package/dist/provider-question.d.ts +19 -0
- package/dist/quota-freshness.d.ts +65 -0
- package/dist/role-launch.d.ts +11 -0
- package/dist/session-launch.d.ts +3 -22
- package/dist/slot-proposal.d.ts +14 -0
- package/dist/task-source-phase.d.ts +75 -0
- package/dist/turn-evidence.d.ts +24 -12
- package/dist/turn-ipc-mission.d.ts +4 -0
- package/dist/turn-ipc-queue.d.ts +19 -0
- package/dist/turn-ipc.d.ts +2 -2
- package/dist/types.d.ts +1 -0
- package/dist/worker-protocol.d.ts +1 -1
- package/package.json +1 -1
- package/src/assistant-protocol.ts +247 -6
- package/src/daemon-normalize.ts +7 -5
- package/src/index.ts +7 -0
- package/src/keyed-doc-delta.ts +9 -1
- package/src/launch-defaults.ts +168 -0
- package/src/mesh-tool-names.ts +5 -0
- package/src/mesh-vocabulary.ts +12 -2
- package/src/node-facts.ts +36 -1
- package/src/outbound-message.ts +17 -0
- package/src/path-ownership.ts +4 -6
- package/src/protocol/daemon-server.ts +7 -2
- package/src/protocol/dashboard-server.ts +8 -2
- package/src/provider-approval.ts +59 -0
- package/src/provider-question.ts +31 -0
- package/src/quota-freshness.ts +139 -0
- package/src/role-launch.ts +51 -0
- package/src/session-launch.ts +8 -1
- package/src/slot-proposal.ts +61 -2
- package/src/task-source-phase.ts +61 -0
- package/src/turn-evidence.ts +22 -19
- package/src/turn-ipc-mission.ts +8 -2
- package/src/turn-ipc-queue.ts +29 -1
- package/src/turn-ipc-turn.ts +1 -1
- package/src/turn-ipc.ts +1 -0
- package/src/types.ts +1 -0
- package/src/worker-protocol.ts +4 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quota snapshot freshness — the ONE decision every display surface shares.
|
|
3
|
+
*
|
|
4
|
+
* Why this lives in the leaf package: the dashboard chips (web-core
|
|
5
|
+
* `quotaWindowCue`), the `adhdev quota` CLI (daemon-core `quota/cli.ts`) and the
|
|
6
|
+
* coordinator-facing `mesh_status` fold (mcp-server `summarizeNodeQuota`) each
|
|
7
|
+
* carried a hand-copied version of this rule, and a fix to one left the other
|
|
8
|
+
* two telling a different story about the same snapshot. They all call
|
|
9
|
+
* `assessQuotaFreshness` now; only the WORDING stays per-surface.
|
|
10
|
+
*
|
|
11
|
+
* Pure: plain objects in, plain object out, no clock except the `now` argument.
|
|
12
|
+
*/
|
|
13
|
+
import type { MeshNodeFactsProviderQuota } from './node-facts'
|
|
14
|
+
|
|
15
|
+
export type QuotaFreshnessCue = 'refreshing' | 'stale'
|
|
16
|
+
|
|
17
|
+
export interface QuotaFreshness {
|
|
18
|
+
/** Undefined = nothing to say (a fresh reading, or no retained numbers). */
|
|
19
|
+
cue?: QuotaFreshnessCue
|
|
20
|
+
/**
|
|
21
|
+
* Only the USER can produce a new reading (aged-out Claude statusline;
|
|
22
|
+
* expired CLI access token — the daemon never redeems it). The surface
|
|
23
|
+
* should say what to run, not promise a self-heal.
|
|
24
|
+
*/
|
|
25
|
+
userMustAct: boolean
|
|
26
|
+
/**
|
|
27
|
+
* Unix ms by which the daemon will have looked again, for a `stale` reading
|
|
28
|
+
* the daemon re-checks on its own. Absent when the user must act, when the
|
|
29
|
+
* reading is `refreshing` (a retry is already scheduled), or when the check
|
|
30
|
+
* is already due.
|
|
31
|
+
*/
|
|
32
|
+
nextCheckAt?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* How long past `metadata.retryAtMs` a retry still reads as "in flight". The
|
|
37
|
+
* retry timer fires at retryAtMs and the probe itself takes a few seconds; a
|
|
38
|
+
* retryAtMs well in the past with no newer attempt means no retry is coming.
|
|
39
|
+
*/
|
|
40
|
+
export const QUOTA_RETRY_INFLIGHT_GRACE_MS = 60_000
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The daemon's backfill horizon: a snapshot is re-probed once it is this old
|
|
44
|
+
* even on an idle machine. Mirrors daemon-core `QUOTA_ROUTABLE_MAX_AGE_MS`
|
|
45
|
+
* (quota/refresh.ts); daemon-core's quota-freshness test pins the two equal.
|
|
46
|
+
*/
|
|
47
|
+
export const QUOTA_BACKFILL_HORIZON_MS = 60 * 60 * 1000
|
|
48
|
+
|
|
49
|
+
function hasUsableWindow(window: { usedPercent?: unknown } | null | undefined): boolean {
|
|
50
|
+
return !!window && typeof window.usedPercent === 'number' && Number.isFinite(window.usedPercent)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Does the snapshot carry any renderable number — session/weekly/monthly
|
|
55
|
+
* windows OR per-pool buckets? `monthly` counts on its own: cursor-cli reports
|
|
56
|
+
* ONLY that axis, and leaving it out made a retained cursor reading look
|
|
57
|
+
* numberless. A window without a finite `usedPercent` is unknown, not 0%.
|
|
58
|
+
*/
|
|
59
|
+
function hasReading(quota: MeshNodeFactsProviderQuota): boolean {
|
|
60
|
+
if (hasUsableWindow(quota.session) || hasUsableWindow(quota.weekly) || hasUsableWindow(quota.monthly)) return true
|
|
61
|
+
return Array.isArray(quota.buckets) && quota.buckets.some((b) => !!b && typeof b.usedPercent === 'number' && Number.isFinite(b.usedPercent))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function finiteMs(value: unknown): number | undefined {
|
|
65
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* When the daemon will next look, from the snapshot alone — the same
|
|
70
|
+
* "both clocks" rule the refresh loop's backfill uses (data age AND attempt
|
|
71
|
+
* age must both pass the horizon). Undefined when that moment has passed.
|
|
72
|
+
*/
|
|
73
|
+
function backfillCheckAt(quota: MeshNodeFactsProviderQuota, now: number): number | undefined {
|
|
74
|
+
const dataDue = finiteMs(quota.updatedAt)
|
|
75
|
+
const attempted = finiteMs(quota.metadata?.fetchedAt)
|
|
76
|
+
const dueAt = Math.max(
|
|
77
|
+
dataDue === undefined ? Number.NEGATIVE_INFINITY : dataDue + QUOTA_BACKFILL_HORIZON_MS,
|
|
78
|
+
attempted === undefined ? Number.NEGATIVE_INFINITY : attempted + QUOTA_BACKFILL_HORIZON_MS,
|
|
79
|
+
)
|
|
80
|
+
return Number.isFinite(dueAt) && dueAt > now ? dueAt : undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Which freshness cue a snapshot's retained numbers should carry.
|
|
85
|
+
*
|
|
86
|
+
* - `stale` + `userMustAct` — nothing will refresh it without the user:
|
|
87
|
+
* · `no-data` on any provider EXCEPT codex (Claude's statusline aged out —
|
|
88
|
+
* a session must run). Codex's `no-data` is the daemon re-reading the
|
|
89
|
+
* rollout files itself (≈60s while a CLI is active, ≤60 min idle), so it
|
|
90
|
+
* is `stale` WITHOUT userMustAct;
|
|
91
|
+
* · `expired-token` on Grok/Kimi/antigravity — only the owning CLI
|
|
92
|
+
* renews it on use. An idle CLI has no background renewal cadence.
|
|
93
|
+
* A scheduled quota retry does not itself renew credentials.
|
|
94
|
+
* - `refreshing` — retained numbers after a transient failure with a retry
|
|
95
|
+
* actually pending. ★Honest about the end of the retry budget: once the
|
|
96
|
+
* daemon has stopped retrying (`metadata.retryExhausted`, or `retryAtMs`
|
|
97
|
+
* long past with no newer attempt) the numbers are `stale` with the next
|
|
98
|
+
* scheduled check, not "refreshing" for the up-to-60 minutes until the
|
|
99
|
+
* hourly backfill. A snapshot with NO retry bookkeeping at all (a daemon
|
|
100
|
+
* predating the stamp) keeps the old `refreshing` reading.
|
|
101
|
+
*/
|
|
102
|
+
export function assessQuotaFreshness(quota: MeshNodeFactsProviderQuota, now: number = Date.now()): QuotaFreshness {
|
|
103
|
+
const meta = quota.metadata
|
|
104
|
+
const kind = meta?.failureKind
|
|
105
|
+
const userMustAct = kind === 'no-data' && quota.provider !== 'codex-cli'
|
|
106
|
+
const awaitsCliUse = kind === 'expired-token' && ['grok-cli', 'kimi', 'antigravity-cli'].includes(quota.provider)
|
|
107
|
+
// Order matters: the aged-out Claude shape and the retained expired-token
|
|
108
|
+
// shape both ALSO mark lastGoodWindows, so they must be classified before
|
|
109
|
+
// the generic branch. An expired short-lived access token that the CLI
|
|
110
|
+
// renews itself is NOT an auth failure requiring user action (userMustAct:
|
|
111
|
+
// false), while any retained lastGood numbers are marked stale.
|
|
112
|
+
if (userMustAct) return hasReading(quota) ? { cue: 'stale', userMustAct: true } : { userMustAct: true }
|
|
113
|
+
if (awaitsCliUse) return hasReading(quota) ? { cue: 'stale', userMustAct: false } : { userMustAct: false }
|
|
114
|
+
if (meta?.lastGoodWindows !== true) {
|
|
115
|
+
// A successful fetch is not a perpetual live reading. Use the DATA
|
|
116
|
+
// clock; a recent failed attempt must never rejuvenate old numbers.
|
|
117
|
+
const measuredAt = finiteMs(quota.updatedAt)
|
|
118
|
+
if (hasReading(quota) && (measuredAt === undefined || now - measuredAt >= QUOTA_BACKFILL_HORIZON_MS)) {
|
|
119
|
+
return { cue: 'stale', userMustAct }
|
|
120
|
+
}
|
|
121
|
+
return { userMustAct }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const retryAtMs = finiteMs(meta?.retryAtMs)
|
|
125
|
+
const retryStopped = meta?.retryExhausted === true
|
|
126
|
+
|| (retryAtMs !== undefined && retryAtMs + QUOTA_RETRY_INFLIGHT_GRACE_MS < now)
|
|
127
|
+
// A `no-data` retained reading (codex) has no retry at all — it is a
|
|
128
|
+
// snapshot the daemon will simply look at again, never an in-flight refresh.
|
|
129
|
+
if (kind !== 'no-data' && !retryStopped) return { cue: 'refreshing', userMustAct: false }
|
|
130
|
+
const nextCheckAt = backfillCheckAt(quota, now)
|
|
131
|
+
return nextCheckAt === undefined
|
|
132
|
+
? { cue: 'stale', userMustAct: false }
|
|
133
|
+
: { cue: 'stale', userMustAct: false, nextCheckAt }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Whole minutes (min 1) until `nextCheckAt`, for the "next check ~Nm" wording. */
|
|
137
|
+
export function minutesUntilQuotaCheck(nextCheckAt: number, now: number = Date.now()): number {
|
|
138
|
+
return Math.max(1, Math.round((nextCheckAt - now) / 60_000))
|
|
139
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Persisted role-launch intent. Restart replays this selection rather than
|
|
2
|
+
* consulting newer machine defaults; restore preserves its original provenance. */
|
|
3
|
+
import { isLaunchDefaultRole, isLaunchDefaultSource, type LaunchDefaultRole,
|
|
4
|
+
type LaunchDefaultSelection, type LaunchDefaultAxis, type LaunchDefaultRef } from './launch-defaults';
|
|
5
|
+
export interface RoleLaunchSelection {
|
|
6
|
+
role: LaunchDefaultRole;
|
|
7
|
+
selection: LaunchDefaultSelection;
|
|
8
|
+
applied: Record<keyof LaunchDefaultSelection, LaunchDefaultAxis>;
|
|
9
|
+
defaultRef?: LaunchDefaultRef;
|
|
10
|
+
documentRevision: number;
|
|
11
|
+
}
|
|
12
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
13
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
14
|
+
? value as Record<string, unknown> : undefined;
|
|
15
|
+
}
|
|
16
|
+
function ref(value: unknown): LaunchDefaultRef | undefined {
|
|
17
|
+
const r = record(value);
|
|
18
|
+
if (!r || !isLaunchDefaultRole(r.role) || (r.scope !== 'global' && r.scope !== 'project')
|
|
19
|
+
|| !Number.isSafeInteger(r.revision) || (r.revision as number) < 1
|
|
20
|
+
|| (r.scope === 'project' && (typeof r.projectId !== 'string' || !r.projectId))) return undefined;
|
|
21
|
+
return { role: r.role, scope: r.scope, revision: r.revision as number,
|
|
22
|
+
...(r.scope === 'project' ? { projectId: r.projectId as string } : {}) };
|
|
23
|
+
}
|
|
24
|
+
function axis(value: unknown): LaunchDefaultAxis | undefined {
|
|
25
|
+
const a = record(value);
|
|
26
|
+
if (!a || !isLaunchDefaultSource(a.source) || (a.value !== null && typeof a.value !== 'string')) return undefined;
|
|
27
|
+
const defaultRef = ref(a.defaultRef);
|
|
28
|
+
if (a.source === 'stored_default' && defaultRef?.scope !== 'global'
|
|
29
|
+
|| a.source === 'project_default' && defaultRef?.scope !== 'project'
|
|
30
|
+
|| (a.source === 'user' || a.source === 'provider_default') && a.defaultRef !== undefined) return undefined;
|
|
31
|
+
return { value: a.value as string | null, source: a.source, ...(defaultRef ? { defaultRef } : {}) };
|
|
32
|
+
}
|
|
33
|
+
export function parseRoleLaunchSelection(value: unknown): RoleLaunchSelection | undefined {
|
|
34
|
+
const r = record(value), s = record(r?.selection), a = record(r?.applied);
|
|
35
|
+
if (!r || !isLaunchDefaultRole(r.role) || !s || typeof s.provider !== 'string' || !s.provider
|
|
36
|
+
|| !Number.isSafeInteger(r.documentRevision) || (r.documentRevision as number) < 1
|
|
37
|
+
|| (s.model !== undefined && typeof s.model !== 'string')
|
|
38
|
+
|| (s.thinkingLevel !== undefined && typeof s.thinkingLevel !== 'string')) return undefined;
|
|
39
|
+
const provider = axis(a?.provider), model = axis(a?.model), thinkingLevel = axis(a?.thinkingLevel);
|
|
40
|
+
if (!provider || !model || !thinkingLevel
|
|
41
|
+
|| [provider, model, thinkingLevel].some(axis => axis.defaultRef && (axis.defaultRef.role !== r.role
|
|
42
|
+
|| axis.defaultRef.scope === 'global' && axis.defaultRef.revision > (r.documentRevision as number)))
|
|
43
|
+
|| provider.value !== s.provider
|
|
44
|
+
|| model.value !== (s.model ?? null) || thinkingLevel.value !== (s.thinkingLevel ?? null)) return undefined;
|
|
45
|
+
const defaultRef = ref(r.defaultRef);
|
|
46
|
+
return { role: r.role, selection: { provider: s.provider,
|
|
47
|
+
...(typeof s.model === 'string' ? { model: s.model } : {}),
|
|
48
|
+
...(typeof s.thinkingLevel === 'string' ? { thinkingLevel: s.thinkingLevel } : {}) },
|
|
49
|
+
applied: { provider, model, thinkingLevel }, ...(defaultRef ? { defaultRef } : {}),
|
|
50
|
+
documentRevision: r.documentRevision as number };
|
|
51
|
+
}
|
package/src/session-launch.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseRoleLaunchSelection, type RoleLaunchSelection } from './role-launch'
|
|
1
2
|
/**
|
|
2
3
|
* session-launch — how a session was launched: provider, model and thinking
|
|
3
4
|
* level, and WHERE each value came from.
|
|
@@ -35,11 +36,14 @@ export const MODEL_AXIS_SOURCES = [
|
|
|
35
36
|
'user',
|
|
36
37
|
/** The dashboard dialog restored it from the last launch (browser-local memory). */
|
|
37
38
|
'remembered',
|
|
39
|
+
/** The authoritative daemon role default or host project override. */
|
|
40
|
+
'stored_default',
|
|
41
|
+
'project_default',
|
|
38
42
|
/** A mesh capability slot supplied it (or a difficulty preset the slot outranked). */
|
|
39
43
|
'mesh_slot',
|
|
40
44
|
/** An explicit per-task model / thinking level on a mesh queue task. */
|
|
41
45
|
'task_override',
|
|
42
|
-
/** Nothing
|
|
46
|
+
/** Nothing requested: the provider chooses; resolvedDefault records it when known. */
|
|
43
47
|
'provider_default',
|
|
44
48
|
/** A value was requested without saying where it came from, or nothing is known. */
|
|
45
49
|
'unspecified',
|
|
@@ -102,6 +106,7 @@ export interface ModelSelection {
|
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
export interface SessionLaunchRecord {
|
|
109
|
+
roleLaunch?: RoleLaunchSelection
|
|
105
110
|
sessionId: string
|
|
106
111
|
providerType: string
|
|
107
112
|
providerVersion?: string
|
|
@@ -230,7 +235,9 @@ export function parseSessionLaunchRecord(raw: unknown): SessionLaunchRecord | un
|
|
|
230
235
|
const providerVersion = readString(record.providerVersion)
|
|
231
236
|
const workspace = readString(record.workspace)
|
|
232
237
|
const autoApproveModeId = readString(record.autoApproveModeId)
|
|
238
|
+
const roleLaunch = parseRoleLaunchSelection(record.roleLaunch)
|
|
233
239
|
return {
|
|
240
|
+
...(roleLaunch ? { roleLaunch } : {}),
|
|
234
241
|
sessionId,
|
|
235
242
|
providerType,
|
|
236
243
|
...(providerVersion ? { providerVersion } : {}),
|
package/src/slot-proposal.ts
CHANGED
|
@@ -40,6 +40,13 @@ import {
|
|
|
40
40
|
export interface CliSlotRecipe {
|
|
41
41
|
/** Optional model to pin on the slot. Best-effort at launch. */
|
|
42
42
|
model?: string
|
|
43
|
+
/**
|
|
44
|
+
* Optional ordered candidate models to prefer when the provider's live
|
|
45
|
+
* `modelOptions` (from model discovery) is present on `DetectedCliProvider`.
|
|
46
|
+
* The first candidate present in `modelOptions` wins; falls back to `model`
|
|
47
|
+
* when `modelOptions` is absent or contains none of the candidates.
|
|
48
|
+
*/
|
|
49
|
+
modelCandidates?: readonly string[]
|
|
43
50
|
/** Optional thinking level, in the provider's own vocabulary. */
|
|
44
51
|
thinkingLevel?: string
|
|
45
52
|
/** Difficulty range this slot handles. Empty = general-purpose. */
|
|
@@ -101,11 +108,26 @@ export const CLI_SLOT_RECIPES: Readonly<Record<string, readonly CliSlotRecipe[]>
|
|
|
101
108
|
],
|
|
102
109
|
'antigravity-cli': [
|
|
103
110
|
{
|
|
104
|
-
model: 'Gemini 3.
|
|
111
|
+
model: 'Gemini 3.8 Flash (High)',
|
|
112
|
+
modelCandidates: ['Gemini 3.8 Flash (High)', 'Gemini 4 Argon (Low)', 'Gemini 3.7 Flash (High)', 'Gemini 3.1 Pro (High)'],
|
|
105
113
|
difficulty: ['easy'],
|
|
106
114
|
maxParallel: 2,
|
|
107
115
|
rationale: 'Cheap capacity for easy tasks.',
|
|
108
116
|
},
|
|
117
|
+
{
|
|
118
|
+
model: 'Gemini 4 Argon (Medium)',
|
|
119
|
+
modelCandidates: ['Gemini 4 Argon (Medium)', 'Gemini 3.8 Flash (High)', 'Gemini 3.7 Flash (High)', 'Gemini 3.1 Pro (High)'],
|
|
120
|
+
difficulty: ['medium'],
|
|
121
|
+
maxParallel: 2,
|
|
122
|
+
rationale: 'Balanced Argon tier for medium tasks.',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
model: 'Gemini 4 Argon (High)',
|
|
126
|
+
modelCandidates: ['Gemini 4 Argon (High)', 'Gemini 3.8 Flash (High)', 'Gemini 3.1 Pro (High)'],
|
|
127
|
+
difficulty: ['difficult'],
|
|
128
|
+
maxParallel: 1,
|
|
129
|
+
rationale: 'Reserved for hard tasks; capped at 1 to bound cost.',
|
|
130
|
+
},
|
|
109
131
|
],
|
|
110
132
|
'cursor-cli': [
|
|
111
133
|
{
|
|
@@ -137,6 +159,13 @@ export interface DetectedCliProvider {
|
|
|
137
159
|
displayName?: string
|
|
138
160
|
/** Detected version, when known. Display only — never affects the mapping. */
|
|
139
161
|
version?: string
|
|
162
|
+
/**
|
|
163
|
+
* Live discovered / resolved `modelOptions` for this provider on the node,
|
|
164
|
+
* when available on the status catalog. When present and non-empty, recipes
|
|
165
|
+
* prefer the first matching model from `[recipe.model, ...recipe.modelCandidates]`
|
|
166
|
+
* that exists in `modelOptions`.
|
|
167
|
+
*/
|
|
168
|
+
modelOptions?: readonly string[]
|
|
140
169
|
}
|
|
141
170
|
|
|
142
171
|
/** One proposed slot plus why it was proposed. */
|
|
@@ -197,6 +226,36 @@ function dedupeDetected(detected: readonly DetectedCliProvider[]): DetectedCliPr
|
|
|
197
226
|
return out
|
|
198
227
|
}
|
|
199
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Resolve a recipe's pinned model against the provider's live `modelOptions`
|
|
231
|
+
* (populated from the daemon's model-discovery overlay when available). If
|
|
232
|
+
* `modelOptions` is absent or none of the recipe's candidates appear in it,
|
|
233
|
+
* falls back to `recipe.model`.
|
|
234
|
+
*/
|
|
235
|
+
function resolveRecipeModel(
|
|
236
|
+
recipe: CliSlotRecipe,
|
|
237
|
+
modelOptions: readonly string[] | undefined,
|
|
238
|
+
): string | undefined {
|
|
239
|
+
if (!Array.isArray(modelOptions) || modelOptions.length === 0) {
|
|
240
|
+
return recipe.model
|
|
241
|
+
}
|
|
242
|
+
const available = new Set(
|
|
243
|
+
modelOptions
|
|
244
|
+
.filter((m): m is string => typeof m === 'string' && m.trim().length > 0)
|
|
245
|
+
.map(m => m.trim()),
|
|
246
|
+
)
|
|
247
|
+
if (available.size === 0) return recipe.model
|
|
248
|
+
const candidates = [
|
|
249
|
+
...(recipe.model ? [recipe.model] : []),
|
|
250
|
+
...(recipe.modelCandidates ?? []),
|
|
251
|
+
]
|
|
252
|
+
for (const candidate of candidates) {
|
|
253
|
+
const trimmed = typeof candidate === 'string' ? candidate.trim() : ''
|
|
254
|
+
if (trimmed && available.has(trimmed)) return trimmed
|
|
255
|
+
}
|
|
256
|
+
return recipe.model
|
|
257
|
+
}
|
|
258
|
+
|
|
200
259
|
/**
|
|
201
260
|
* Build a capability-slot proposal from a node's detected CLI providers.
|
|
202
261
|
*
|
|
@@ -229,7 +288,7 @@ export function buildSlotProposal(
|
|
|
229
288
|
// a proposal can never preview a shape the write would reshape.
|
|
230
289
|
const slot = normalizeNodeCapabilitySlot({
|
|
231
290
|
provider: provider.type,
|
|
232
|
-
model: recipe.
|
|
291
|
+
model: resolveRecipeModel(recipe, provider.modelOptions),
|
|
233
292
|
thinkingLevel: recipe.thinkingLevel,
|
|
234
293
|
difficulty: recipe.difficulty,
|
|
235
294
|
maxParallel: recipe.maxParallel,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { OwnedPathEntry } from './path-ownership'
|
|
2
|
+
|
|
3
|
+
export type MeshTaskPhase = 'source' | 'integration'
|
|
4
|
+
|
|
5
|
+
/** Host-produced evidence. Never accepted from MCP/peer-supplied paths or branch labels. */
|
|
6
|
+
export interface PhysicalOwnershipScope {
|
|
7
|
+
version: 1
|
|
8
|
+
repoIdentity: string
|
|
9
|
+
machineId: string
|
|
10
|
+
nodeId: string
|
|
11
|
+
worktree: string
|
|
12
|
+
worktreeIdentity: string
|
|
13
|
+
gitDir: string
|
|
14
|
+
commonGitDir: string
|
|
15
|
+
phase: MeshTaskPhase
|
|
16
|
+
gitResources: { index: string; refs: string; hooks: string; config: string }
|
|
17
|
+
resources: Array<{ path: string; identity?: string; subtree: boolean }>
|
|
18
|
+
declaration: readonly OwnedPathEntry[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A source packet is an input to integration, never release/feature acceptance. */
|
|
22
|
+
export interface MeshSourceCandidate {
|
|
23
|
+
version: 1
|
|
24
|
+
readiness: 'uncommitted' | 'verified_source_candidate'
|
|
25
|
+
taskId: string
|
|
26
|
+
missionId: string
|
|
27
|
+
revision: number
|
|
28
|
+
repoIdentity: string
|
|
29
|
+
machineId: string
|
|
30
|
+
canonicalWorktree: string
|
|
31
|
+
rootBaseSha: string
|
|
32
|
+
ossBaseSha: string
|
|
33
|
+
rootCandidateSha?: string
|
|
34
|
+
ossCandidateSha?: string
|
|
35
|
+
patchSha256: string
|
|
36
|
+
manifestSha256: string
|
|
37
|
+
touchedPaths: string[]
|
|
38
|
+
tests: Array<{ command: string; exit: number; logSha256: string }>
|
|
39
|
+
settledCommands: boolean
|
|
40
|
+
integrationTaskId: string
|
|
41
|
+
logicalOverlapTaskIds: string[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Product B consumes one frozen batch; normal hooks/parity/gates remain required. */
|
|
45
|
+
export interface MeshIntegrationBatchContract {
|
|
46
|
+
version: 1
|
|
47
|
+
batchId: string
|
|
48
|
+
candidates: MeshSourceCandidate[]
|
|
49
|
+
integrationOrder: string[]
|
|
50
|
+
approvedRootSha: string
|
|
51
|
+
approvedOssSha: string
|
|
52
|
+
bundleInvocationCount: 1
|
|
53
|
+
requiredGates: string[]
|
|
54
|
+
normalHooksRequired: true
|
|
55
|
+
independentReviewRequired: true
|
|
56
|
+
ownerLandingApprovalRequired: true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** These only rank legal targets; targetNodeId/SessionId and requiredTags remain hard. */
|
|
60
|
+
export interface MeshRoutingPreferences { nodeIds?: string[]; providers?: string[] }
|
|
61
|
+
export interface MeshReviewConstraint { sourceTaskId: string; differentProvider: true }
|
package/src/turn-evidence.ts
CHANGED
|
@@ -83,9 +83,9 @@ export type SessionReboundReason = typeof SESSION_REBOUND_REASONS[number]
|
|
|
83
83
|
|
|
84
84
|
export const SUSPENSION_MODALS = ['approval', 'choice'] as const
|
|
85
85
|
export type SuspensionModal = typeof SUSPENSION_MODALS[number]
|
|
86
|
-
export const SUSPENSION_RESOLUTIONS = ['approved', 'rejected', 'answered'] as const
|
|
86
|
+
export const SUSPENSION_RESOLUTIONS = ['approved', 'rejected', 'answered', 'dismissed'] as const
|
|
87
87
|
export type SuspensionResolution = typeof SUSPENSION_RESOLUTIONS[number]
|
|
88
|
-
export const SUSPENSION_RESOLUTION_VIAS = ['modal_button', 'auto_approve', 'prompt_answer'] as const
|
|
88
|
+
export const SUSPENSION_RESOLUTION_VIAS = ['modal_button', 'auto_approve', 'prompt_answer', 'modal_disappeared'] as const
|
|
89
89
|
export type SuspensionResolutionVia = typeof SUSPENSION_RESOLUTION_VIAS[number]
|
|
90
90
|
|
|
91
91
|
export const TURN_END_STRENGTHS = ['genuine', 'weak'] as const
|
|
@@ -97,13 +97,13 @@ export const TURN_END_BLOCK_REASONS = [
|
|
|
97
97
|
] as const
|
|
98
98
|
export type TurnEndBlockReason = typeof TURN_END_BLOCK_REASONS[number]
|
|
99
99
|
|
|
100
|
-
export const NATIVE_TURN_OUTCOMES = ['completed', 'aborted'] as const
|
|
100
|
+
export const NATIVE_TURN_OUTCOMES = ['completed', 'aborted', 'failed'] as const
|
|
101
101
|
export type NativeTurnOutcome = typeof NATIVE_TURN_OUTCOMES[number]
|
|
102
102
|
|
|
103
103
|
export const LIVENESS_RESULTS = ['alive', 'unknown', 'read_failed', 'dead'] as const
|
|
104
104
|
export type LivenessResult = typeof LIVENESS_RESULTS[number]
|
|
105
105
|
|
|
106
|
-
export const PROVIDER_FAILURES = ['auth_failed', 'billing_failed'] as const
|
|
106
|
+
export const PROVIDER_FAILURES = ['auth_failed', 'billing_failed', 'usage_limit_exceeded', 'server_overloaded', 'provider_error'] as const
|
|
107
107
|
export type ProviderFailure = typeof PROVIDER_FAILURES[number]
|
|
108
108
|
|
|
109
109
|
/**
|
|
@@ -150,11 +150,11 @@ export const HOLD_REASONS = [
|
|
|
150
150
|
// — a turn_end commits the report (R9t), expiry commits it anyway (R13t) so
|
|
151
151
|
// a session whose FSM never shows the idle edge again does not wait for
|
|
152
152
|
// liveness / hard_ceiling.
|
|
153
|
-
'await_end',
|
|
153
|
+
'await_end', 'report_correction',
|
|
154
154
|
] as const
|
|
155
155
|
export type HoldReason = typeof HOLD_REASONS[number]
|
|
156
156
|
|
|
157
|
-
export const TURN_OUTCOMES = ['completed', 'failed', 'cancelled'] as const
|
|
157
|
+
export const TURN_OUTCOMES = ['completed', 'blocked', 'failed', 'cancelled'] as const
|
|
158
158
|
export type TurnOutcome = typeof TURN_OUTCOMES[number]
|
|
159
159
|
|
|
160
160
|
export const COMMIT_STRENGTHS = ['genuine', 'weak', 'tool_report', 'operator'] as const
|
|
@@ -164,7 +164,7 @@ export type CommitStrength = typeof COMMIT_STRENGTHS[number]
|
|
|
164
164
|
export const TURN_REASONS = [
|
|
165
165
|
// commits
|
|
166
166
|
'turn_end', 'weak_end_confirmed', 'transcript_final', 'worker_reported',
|
|
167
|
-
'finalization_timeout_no_response', 'session_error', 'provider_auth_failed', 'provider_billing_failed',
|
|
167
|
+
'finalization_timeout_no_response', 'weak_end_liveness_unconfirmed', 'weak_end_probe_unobserved', 'report_correction_expired', 'session_error', 'provider_auth_failed', 'provider_billing_failed', 'provider_usage_limit_exceeded', 'provider_server_overloaded', 'provider_error',
|
|
168
168
|
'operator_cancel', 'intentional_cleanup', 'task_removed', 'mission_abandoned', 'superseded',
|
|
169
169
|
'operator_update', 'refine_terminal', 'validation_terminal',
|
|
170
170
|
'hard_ceiling', 'reclaim_budget_exhausted', 'hollow_max_retries', 'migration_orphan',
|
|
@@ -186,7 +186,7 @@ export type TurnReason = typeof TURN_REASONS[number]
|
|
|
186
186
|
* owner's local `turn_events.payload_json` and is rendered at deliver time.
|
|
187
187
|
*/
|
|
188
188
|
export const NOTIFY_KINDS = [
|
|
189
|
-
'completed', 'failed', 'cancelled', 'stopped', 'approval', 'choice', 'approval_resolved',
|
|
189
|
+
'completed', 'blocked', 'failed', 'cancelled', 'stopped', 'approval', 'choice', 'approval_resolved',
|
|
190
190
|
'candidate', 'no_progress', 'progress', 'late_completion', 'mesh_event',
|
|
191
191
|
] as const
|
|
192
192
|
export type NotifyKind = typeof NOTIFY_KINDS[number]
|
|
@@ -207,7 +207,7 @@ export interface LiveTurnPending { modal: boolean; adapterPending: boolean; trai
|
|
|
207
207
|
/** Structural copy of daemon-core's `CoordinatorIdentity` (this package cannot import daemon-core). */
|
|
208
208
|
export interface EvidenceCoordinatorIdentity { daemonId: string; coordinatorRunId: string; sessionId?: string }
|
|
209
209
|
|
|
210
|
-
export interface NativeTurnMarkerRef { outcome: NativeTurnOutcome; turnId?: string }
|
|
210
|
+
export interface NativeTurnMarkerRef { outcome: NativeTurnOutcome; turnId?: string; receivedAt?: number; startedAt?: number; errorAt?: number }
|
|
211
211
|
|
|
212
212
|
export interface EvidenceEnvelope {
|
|
213
213
|
/** Stable id of this observation; dedupe key across republish/replication. */
|
|
@@ -250,10 +250,10 @@ export type TurnEvidenceBody =
|
|
|
250
250
|
| { kind: 'session_rebound'; toSessionId: string; reason: SessionReboundReason }
|
|
251
251
|
// ── worker turn lifecycle ──
|
|
252
252
|
| { kind: 'turn_started'; retro: boolean }
|
|
253
|
-
| { kind: 'suspension'; modal: SuspensionModal; modalKey?: string }
|
|
254
|
-
| { kind: 'suspension_resolved'; resolution: SuspensionResolution; via: SuspensionResolutionVia }
|
|
253
|
+
| { kind: 'suspension'; modal: SuspensionModal; modalKey?: string; providerSetup?: boolean; clockUnknown?: boolean }
|
|
254
|
+
| { kind: 'suspension_resolved'; resolution: SuspensionResolution; via: SuspensionResolutionVia; modalKey?: string }
|
|
255
255
|
| { kind: 'turn_end'; strength: TurnEndStrength; afterFinalizationTimeout?: boolean; hollow?: boolean; summary?: SummaryRef
|
|
256
|
-
blockReason?: TurnEndBlockReason; releasedByHardCap?: boolean; nativeOutcome?: NativeTurnOutcome; live?: LiveTurnPending
|
|
256
|
+
blockReason?: TurnEndBlockReason; releasedByHardCap?: boolean; nativeOutcome?: NativeTurnOutcome; nativeMarker?: NativeTurnMarkerRef; live?: LiveTurnPending
|
|
257
257
|
/**
|
|
258
258
|
* Stamped (true only) by the WORKER's daemon when the session holds a live
|
|
259
259
|
* worker-MCP session bind, i.e. the worker can call report_completion. The
|
|
@@ -273,7 +273,7 @@ export type TurnEvidenceBody =
|
|
|
273
273
|
live: LiveTurnPending; summary?: SummaryRef; messageAt?: number }
|
|
274
274
|
| { kind: 'transcript_activity'; newestActivityAt: number }
|
|
275
275
|
| { kind: 'no_progress'; stalledMs: number; observedStatus: NoProgressObservedStatus; finalAssistantPresent: boolean }
|
|
276
|
-
| { kind: 'liveness'; result: LivenessResult }
|
|
276
|
+
| { kind: 'liveness'; result: LivenessResult; probeStartedAtMs?: number }
|
|
277
277
|
| { kind: 'process_exit'; exitCode: number | null; providerFailure?: ProviderFailure }
|
|
278
278
|
| { kind: 'session_error'; reason: SessionErrorReason }
|
|
279
279
|
// ── worker MCP (F2) ──
|
|
@@ -284,6 +284,7 @@ export type TurnEvidenceBody =
|
|
|
284
284
|
*/
|
|
285
285
|
| { kind: 'worker_report'; outcome: WorkerReportOutcome; summary?: SummaryRef; hasHandoffNotes: boolean
|
|
286
286
|
branchState?: WorkerBranchState; touchedFileCount?: number }
|
|
287
|
+
| { kind: 'worker_report_correction_requested'; deliveryId?: string }
|
|
287
288
|
| { kind: 'worker_progress'; note?: SummaryRef }
|
|
288
289
|
// ── coordinator / operator ──
|
|
289
290
|
| { kind: 'git_side_effect'; dirty: boolean; commitsSinceDispatch: number; attributable: boolean }
|
|
@@ -352,13 +353,13 @@ export const TURN_EVIDENCE_FIELD_SPECS: TurnEvidenceFieldSpecs = {
|
|
|
352
353
|
duplicate_dispatch_refusal: { holderSessionId: id, holderAttemptId: idOpt },
|
|
353
354
|
session_rebound: { toSessionId: id, reason: en(SESSION_REBOUND_REASONS) },
|
|
354
355
|
turn_started: { retro: bool },
|
|
355
|
-
suspension: { modal: en(SUSPENSION_MODALS), modalKey: idOpt },
|
|
356
|
-
suspension_resolved: { resolution: en(SUSPENSION_RESOLUTIONS), via: en(SUSPENSION_RESOLUTION_VIAS) },
|
|
356
|
+
suspension: { modal: en(SUSPENSION_MODALS), modalKey: idOpt, providerSetup: boolOpt, clockUnknown: boolOpt },
|
|
357
|
+
suspension_resolved: { resolution: en(SUSPENSION_RESOLUTIONS), via: en(SUSPENSION_RESOLUTION_VIAS), modalKey: idOpt },
|
|
357
358
|
turn_end: {
|
|
358
359
|
strength: en(TURN_END_STRENGTHS), afterFinalizationTimeout: boolOpt, hollow: boolOpt,
|
|
359
360
|
summary: { t: 'summary_ref', optional: true }, blockReason: enOpt(TURN_END_BLOCK_REASONS),
|
|
360
361
|
releasedByHardCap: boolOpt, nativeOutcome: enOpt(NATIVE_TURN_OUTCOMES), live: { t: 'live', optional: true },
|
|
361
|
-
reportExpected: boolOpt, providerFailure: enOpt(PROVIDER_FAILURES),
|
|
362
|
+
nativeMarker: { t: 'native_marker', optional: true }, reportExpected: boolOpt, providerFailure: enOpt(PROVIDER_FAILURES),
|
|
362
363
|
},
|
|
363
364
|
transcript_final: {
|
|
364
365
|
selfAttributing: bool, nativeRead: bool, nativeMarker: { t: 'native_marker', optional: true },
|
|
@@ -366,13 +367,14 @@ export const TURN_EVIDENCE_FIELD_SPECS: TurnEvidenceFieldSpecs = {
|
|
|
366
367
|
},
|
|
367
368
|
transcript_activity: { newestActivityAt: ms },
|
|
368
369
|
no_progress: { stalledMs: ms, observedStatus: en(NO_PROGRESS_OBSERVED_STATUSES), finalAssistantPresent: bool },
|
|
369
|
-
liveness: { result: en(LIVENESS_RESULTS) },
|
|
370
|
+
liveness: { result: en(LIVENESS_RESULTS), probeStartedAtMs: msOpt },
|
|
370
371
|
process_exit: { exitCode: { t: 'int', nullable: true }, providerFailure: enOpt(PROVIDER_FAILURES) },
|
|
371
372
|
session_error: { reason: en(SESSION_ERROR_REASONS) },
|
|
372
373
|
worker_report: {
|
|
373
374
|
outcome: en(WORKER_REPORT_OUTCOMES), summary: { t: 'summary_ref', optional: true }, hasHandoffNotes: bool,
|
|
374
375
|
branchState: enOpt(WORKER_BRANCH_STATES), touchedFileCount: intOpt,
|
|
375
376
|
},
|
|
377
|
+
worker_report_correction_requested: { deliveryId: { t: 'id', optional: true } },
|
|
376
378
|
worker_progress: { note: { t: 'summary_ref', optional: true } },
|
|
377
379
|
git_side_effect: { dirty: bool, commitsSinceDispatch: { t: 'int' }, attributable: bool },
|
|
378
380
|
cancel: { reason: en(CANCEL_REASONS) },
|
|
@@ -440,11 +442,12 @@ function isLiveTurnPending(value: unknown): value is LiveTurnPending {
|
|
|
440
442
|
&& (value.newestActivityAt === undefined || isFiniteNumber(value.newestActivityAt))
|
|
441
443
|
}
|
|
442
444
|
|
|
443
|
-
function isNativeMarkerRef(value: unknown): value is NativeTurnMarkerRef {
|
|
445
|
+
export function isNativeMarkerRef(value: unknown): value is NativeTurnMarkerRef {
|
|
444
446
|
return isRecord(value)
|
|
445
|
-
&& hasOnlyKeys(value, ['outcome', 'turnId'])
|
|
447
|
+
&& hasOnlyKeys(value, ['outcome', 'turnId', 'receivedAt', 'startedAt', 'errorAt'])
|
|
446
448
|
&& (NATIVE_TURN_OUTCOMES as readonly string[]).includes(value.outcome as string)
|
|
447
449
|
&& (value.turnId === undefined || isEvidenceIdentifier(value.turnId))
|
|
450
|
+
&& ['receivedAt', 'startedAt', 'errorAt'].every((key) => value[key] === undefined || isFiniteNumber(value[key]))
|
|
448
451
|
}
|
|
449
452
|
|
|
450
453
|
function isCoordinatorIdentity(value: unknown): value is EvidenceCoordinatorIdentity {
|
package/src/turn-ipc-mission.ts
CHANGED
|
@@ -221,6 +221,8 @@ export interface MeshMissionTaskAggregateWire {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
export interface MeshMissionStatsWire {
|
|
224
|
+
/** Present on peers that project proven blocked outcomes separately. */
|
|
225
|
+
blocked?: number
|
|
224
226
|
missionId: string
|
|
225
227
|
taskCount: number
|
|
226
228
|
completed: number
|
|
@@ -313,12 +315,13 @@ function isMeshMissionTaskAggregateWire(value: unknown): value is MeshMissionTas
|
|
|
313
315
|
}
|
|
314
316
|
|
|
315
317
|
function isMeshMissionStatsWire(value: unknown): value is MeshMissionStatsWire {
|
|
316
|
-
const keys = ['missionId', 'taskCount', 'completed', 'failed', 'totalDurationMs', 'wallClockMs', 'retries', 'incompleteTaskIds'] as const
|
|
318
|
+
const keys = ['missionId', 'taskCount', 'completed', 'failed', 'blocked', 'totalDurationMs', 'wallClockMs', 'retries', 'incompleteTaskIds'] as const
|
|
317
319
|
if (!isRecord(value) || !hasOnlyKeys(value, keys)) return false
|
|
318
320
|
if (!isEvidenceIdentifier(value.missionId)) return false
|
|
319
321
|
for (const k of ['taskCount', 'completed', 'failed', 'totalDurationMs', 'retries'] as const) {
|
|
320
322
|
if (!isNonNegativeInt(value[k])) return false
|
|
321
323
|
}
|
|
324
|
+
if (value.blocked !== undefined && !isNonNegativeInt(value.blocked)) return false
|
|
322
325
|
if (value.wallClockMs !== null && !isNonNegativeInt(value.wallClockMs)) return false
|
|
323
326
|
if (!Array.isArray(value.incompleteTaskIds) || !value.incompleteTaskIds.every(isEvidenceIdentifier)) return false
|
|
324
327
|
return true
|
|
@@ -580,6 +583,8 @@ export interface LedgerQueryEntryWire {
|
|
|
580
583
|
}
|
|
581
584
|
|
|
582
585
|
export interface LedgerQuerySummaryWire {
|
|
586
|
+
/** Absent on older peers; never folded into failed by the decoder. */
|
|
587
|
+
taskBlocked?: number
|
|
583
588
|
meshId: string
|
|
584
589
|
totalEntries: number
|
|
585
590
|
taskDispatched: number
|
|
@@ -625,12 +630,13 @@ function isLedgerQueryEntryWire(value: unknown): value is LedgerQueryEntryWire {
|
|
|
625
630
|
}
|
|
626
631
|
|
|
627
632
|
function isLedgerQuerySummaryWire(value: unknown): value is LedgerQuerySummaryWire {
|
|
628
|
-
const keys = ['meshId', 'totalEntries', 'taskDispatched', 'taskCompleted', 'taskFailed', 'taskStalled', 'sessionLaunched', 'checkpointCreated', 'lastActivityAt', 'recentFailures'] as const
|
|
633
|
+
const keys = ['meshId', 'totalEntries', 'taskDispatched', 'taskCompleted', 'taskFailed', 'taskBlocked', 'taskStalled', 'sessionLaunched', 'checkpointCreated', 'lastActivityAt', 'recentFailures'] as const
|
|
629
634
|
if (!isRecord(value) || !hasOnlyKeys(value, keys)) return false
|
|
630
635
|
if (!isEvidenceIdentifier(value.meshId)) return false
|
|
631
636
|
for (const k of ['totalEntries', 'taskDispatched', 'taskCompleted', 'taskFailed', 'taskStalled', 'sessionLaunched', 'checkpointCreated', 'recentFailures'] as const) {
|
|
632
637
|
if (!isNonNegativeInt(value[k])) return false
|
|
633
638
|
}
|
|
639
|
+
if (value.taskBlocked !== undefined && !isNonNegativeInt(value.taskBlocked)) return false
|
|
634
640
|
if (value.lastActivityAt !== null && typeof value.lastActivityAt !== 'string') return false
|
|
635
641
|
return true
|
|
636
642
|
}
|
package/src/turn-ipc-queue.ts
CHANGED
|
@@ -135,6 +135,8 @@ export interface QueueQueryResponse {
|
|
|
135
135
|
counts?: Record<string, number>
|
|
136
136
|
/** `withCounts` + `historicalOlderThanMs`: terminal rows older than that age. */
|
|
137
137
|
oldHistoricalCount?: number
|
|
138
|
+
/** Included in counts.blocked, derived from proven worker reports; stored failed rows stay unchanged. */
|
|
139
|
+
legacyReportedBlockedCount?: number
|
|
138
140
|
/** `withDependencyHeads`. */
|
|
139
141
|
dependencyHeads?: readonly QueueDependencyHeadWire[]
|
|
140
142
|
}
|
|
@@ -159,9 +161,10 @@ function isQueueDependencyHeadWire(value: unknown): value is QueueDependencyHead
|
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
function isQueueQueryResponse(value: unknown): value is QueueQueryResponse {
|
|
162
|
-
if (!isRecord(value) || !hasOnlyKeys(value, ['entries', 'counts', 'oldHistoricalCount', 'dependencyHeads'])) return false
|
|
164
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ['entries', 'counts', 'oldHistoricalCount', 'legacyReportedBlockedCount', 'dependencyHeads'])) return false
|
|
163
165
|
if (!Array.isArray(value.entries) || !value.entries.every(isQueueEntryWire)) return false
|
|
164
166
|
if (value.counts !== undefined && (!isRecord(value.counts) || !Object.values(value.counts).every(isNonNegativeInt))) return false
|
|
167
|
+
if (value.legacyReportedBlockedCount !== undefined && !isNonNegativeInt(value.legacyReportedBlockedCount)) return false
|
|
165
168
|
if (value.oldHistoricalCount !== undefined && !isNonNegativeInt(value.oldHistoricalCount)) return false
|
|
166
169
|
if (value.dependencyHeads !== undefined && (!Array.isArray(value.dependencyHeads) || !value.dependencyHeads.every(isQueueDependencyHeadWire))) return false
|
|
167
170
|
return true
|
|
@@ -491,3 +494,28 @@ function isRecoveryContextQueryResponse(value: unknown): value is RecoveryContex
|
|
|
491
494
|
export function decodeRecoveryContextQueryResponse(value: unknown): RecoveryContextQueryResponse | null {
|
|
492
495
|
return isRecoveryContextQueryResponse(value) ? value : null
|
|
493
496
|
}
|
|
497
|
+
|
|
498
|
+
// ── queue_revise: host-authenticated pending CAS, never a force requeue ──
|
|
499
|
+
export interface QueueReviseRequest {
|
|
500
|
+
v: typeof TURN_IPC_PROTOCOL_VERSION
|
|
501
|
+
meshId: string
|
|
502
|
+
taskId: string
|
|
503
|
+
expectedRevision: number
|
|
504
|
+
patch: { message?: string; ownedPaths?: string[]; phase?: 'source' | 'integration'; dependsOn?: string[]; routingPreferences?: import('./task-source-phase').MeshRoutingPreferences; review?: import('./task-source-phase').MeshReviewConstraint }
|
|
505
|
+
}
|
|
506
|
+
export type QueueReviseResponse = QueueRequeueResponse
|
|
507
|
+
export function decodeQueueReviseRequest(value: unknown): QueueReviseRequest | null {
|
|
508
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'taskId', 'expectedRevision', 'patch'])
|
|
509
|
+
|| value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId) || !isEvidenceIdentifier(value.taskId)
|
|
510
|
+
|| !Number.isSafeInteger(value.expectedRevision) || (value.expectedRevision as number) < 0
|
|
511
|
+
|| !isRecord(value.patch) || !Object.keys(value.patch).length
|
|
512
|
+
|| !hasOnlyKeys(value.patch, ['message', 'ownedPaths', 'phase', 'dependsOn', 'routingPreferences', 'review'])) return null
|
|
513
|
+
const p = value.patch
|
|
514
|
+
if (p.message !== undefined && (typeof p.message !== 'string' || !p.message.trim())) return null
|
|
515
|
+
if (p.phase !== undefined && p.phase !== 'source' && p.phase !== 'integration') return null
|
|
516
|
+
for (const key of ['ownedPaths', 'dependsOn']) {
|
|
517
|
+
if (p[key] !== undefined && (!Array.isArray(p[key]) || !(p[key] as unknown[]).every(x => typeof x === 'string' && !!x.trim()))) return null
|
|
518
|
+
}
|
|
519
|
+
return value as unknown as QueueReviseRequest
|
|
520
|
+
}
|
|
521
|
+
export const decodeQueueReviseResponse = decodeQueueRequeueResponse
|
package/src/turn-ipc-turn.ts
CHANGED
|
@@ -66,7 +66,7 @@ function isTurnObserveResponse(value: unknown): value is TurnObserveResponse {
|
|
|
66
66
|
&& (value.outcome === undefined || isTurnOutcomeValue(value.outcome))
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
const TURN_OUTCOMES_LOCAL = ['completed', 'failed', 'cancelled'] as const
|
|
69
|
+
const TURN_OUTCOMES_LOCAL = ['completed', 'blocked', 'failed', 'cancelled'] as const
|
|
70
70
|
function isTurnOutcomeValue(value: unknown): value is TurnOutcome {
|
|
71
71
|
return typeof value === 'string' && (TURN_OUTCOMES_LOCAL as readonly string[]).includes(value)
|
|
72
72
|
}
|