@raidiant/notifai 10.0.0 → 10.1.1
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 +31 -0
- package/README.md +4 -1
- package/dist/cli-bin.d.ts +21 -1
- package/dist/cli-bin.js +141 -10
- package/dist/cli-bin.js.map +1 -1
- package/dist/cli-update-contract.d.ts +7 -0
- package/dist/cli-update-contract.js +10 -0
- package/dist/cli-update-contract.js.map +1 -0
- package/dist/client.d.ts +6 -1
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/codex-wake.d.ts +2 -0
- package/dist/codex-wake.js +2 -1
- package/dist/codex-wake.js.map +1 -1
- package/dist/commands-auth.d.ts +15 -1
- package/dist/commands-auth.js +32 -14
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-core.d.ts +10 -1
- package/dist/commands-core.js +13 -4
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-devices.d.ts +10 -1
- package/dist/commands-devices.js +17 -13
- package/dist/commands-devices.js.map +1 -1
- package/dist/commands-doctor.js +158 -71
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-harness-context.js +8 -0
- package/dist/commands-harness-context.js.map +1 -1
- package/dist/commands-hooks.js +169 -15
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +16 -0
- package/dist/commands-init.js +254 -91
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send.d.ts +1 -1
- package/dist/commands-send.js +121 -35
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-update.d.ts +10 -0
- package/dist/commands-update.js +141 -0
- package/dist/commands-update.js.map +1 -0
- package/dist/commands.d.ts +2 -1
- package/dist/commands.js +2 -1
- package/dist/commands.js.map +1 -1
- package/dist/harnesses.d.ts +2 -2
- package/dist/harnesses.js +22 -2
- package/dist/harnesses.js.map +1 -1
- package/dist/hook-adapter.d.ts +2 -0
- package/dist/hook-adapter.js +12 -1
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +43 -2
- package/dist/hooks.js +163 -21
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.js +15 -9
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.js +20 -23
- package/dist/interactive.js.map +1 -1
- package/dist/main-run.d.ts +1 -0
- package/dist/main-run.js +42 -0
- package/dist/main-run.js.map +1 -0
- package/dist/main.js +10 -31
- package/dist/main.js.map +1 -1
- package/dist/node-floor.d.ts +18 -0
- package/dist/node-floor.js +35 -0
- package/dist/node-floor.js.map +1 -0
- package/dist/openclaw-plugin.d.ts +49 -0
- package/dist/openclaw-plugin.js +350 -0
- package/dist/openclaw-plugin.js.map +1 -0
- package/dist/program.d.ts +2 -1
- package/dist/program.js +31 -13
- package/dist/program.js.map +1 -1
- package/dist/question-settlement-process.d.ts +15 -0
- package/dist/question-settlement-process.js +35 -0
- package/dist/question-settlement-process.js.map +1 -0
- package/dist/readiness.d.ts +25 -1
- package/dist/readiness.js +32 -2
- package/dist/readiness.js.map +1 -1
- package/dist/session-activation.js +1 -1
- package/dist/session-activation.js.map +1 -1
- package/dist/setup-destinations.d.ts +23 -0
- package/dist/setup-destinations.js +44 -0
- package/dist/setup-destinations.js.map +1 -0
- package/dist/ui/help.d.ts +4 -5
- package/dist/ui/help.js +5 -6
- package/dist/ui/help.js.map +1 -1
- package/package.json +2 -2
- package/src/cli-bin.test.ts +145 -7
- package/src/cli-bin.ts +176 -10
- package/src/cli-update-contract.ts +9 -0
- package/src/cli-update.test.ts +125 -0
- package/src/client.ts +7 -0
- package/src/codex-wake.ts +5 -1
- package/src/commands-auth.ts +47 -13
- package/src/commands-core.ts +21 -4
- package/src/commands-devices.ts +23 -16
- package/src/commands-doctor.ts +197 -75
- package/src/commands-harness-context.ts +8 -0
- package/src/commands-hooks.ts +210 -18
- package/src/commands-init.ts +275 -92
- package/src/commands-send.ts +145 -39
- package/src/commands-update.ts +180 -0
- package/src/commands.test.ts +701 -53
- package/src/commands.ts +2 -0
- package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +57 -0
- package/src/harness-integration.test.ts +82 -0
- package/src/harnesses.test.ts +9 -1
- package/src/harnesses.ts +24 -2
- package/src/hook-adapter.ts +14 -0
- package/src/hooks.test.ts +275 -0
- package/src/hooks.ts +270 -18
- package/src/install-hooks.test.ts +208 -0
- package/src/install-hooks.ts +19 -9
- package/src/interactive.test.ts +31 -0
- package/src/interactive.ts +20 -26
- package/src/main-run.ts +45 -0
- package/src/main.ts +9 -33
- package/src/node-floor.test.ts +27 -0
- package/src/node-floor.ts +37 -0
- package/src/openclaw-plugin.ts +409 -0
- package/src/program.test.ts +35 -0
- package/src/program.ts +37 -12
- package/src/question-settlement-process.ts +47 -0
- package/src/readiness.ts +35 -2
- package/src/session-activation.ts +1 -1
- package/src/setup-destinations.test.ts +41 -0
- package/src/setup-destinations.ts +50 -0
- package/src/skill.test.ts +13 -2
- package/src/ui/help.test.ts +16 -0
- package/src/ui/help.ts +6 -7
- package/src/commands-devices.test.ts +0 -15
package/src/hooks.ts
CHANGED
|
@@ -111,6 +111,13 @@ export interface SessionState {
|
|
|
111
111
|
* question the moment a second was registered (2026-08-09).
|
|
112
112
|
*/
|
|
113
113
|
pending?: PendingQuestion[]
|
|
114
|
+
/**
|
|
115
|
+
* Bounded terminal history for stable `q_...` lookup after the live queue is
|
|
116
|
+
* gone. It carries identities and lifecycle state only; question content
|
|
117
|
+
* remains in the ordinary pending/retirement records under their existing
|
|
118
|
+
* retention boundary.
|
|
119
|
+
*/
|
|
120
|
+
question_history?: QuestionHistoryEntry[]
|
|
114
121
|
/**
|
|
115
122
|
* Questions that have been delivered to the user's devices and are now dead,
|
|
116
123
|
* but whose retirement has not been confirmed yet.
|
|
@@ -191,6 +198,8 @@ interface AcceptedAnswerDelivery {
|
|
|
191
198
|
|
|
192
199
|
/** A delivered question awaiting its retirement push. */
|
|
193
200
|
export interface RetiringQuestion {
|
|
201
|
+
/** Stable local identity retained through remote retirement. */
|
|
202
|
+
question_id?: string
|
|
194
203
|
request_id: string
|
|
195
204
|
collapse_key: string
|
|
196
205
|
/** The Device Installations that actually received the question. */
|
|
@@ -273,6 +282,38 @@ export interface PendingQuestion {
|
|
|
273
282
|
submission?: PendingSubmissionIntent
|
|
274
283
|
}
|
|
275
284
|
|
|
285
|
+
export type QuestionDeliveryState =
|
|
286
|
+
| 'local'
|
|
287
|
+
| 'frozen'
|
|
288
|
+
| 'live'
|
|
289
|
+
| 'answered'
|
|
290
|
+
| 'withdrawn'
|
|
291
|
+
| 'retired'
|
|
292
|
+
|
|
293
|
+
type QuestionTerminalState = Extract<QuestionDeliveryState, 'answered' | 'withdrawn' | 'retired'>
|
|
294
|
+
|
|
295
|
+
export interface QuestionHistoryEntry {
|
|
296
|
+
question_id: string
|
|
297
|
+
state: QuestionTerminalState
|
|
298
|
+
/** Present only after local promotion proves the Notification Request exists. */
|
|
299
|
+
request_id?: string
|
|
300
|
+
/** Reserved idempotent identity that was never locally promoted. */
|
|
301
|
+
frozen_request_id?: string
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface QuestionStateView {
|
|
305
|
+
question_id: string
|
|
306
|
+
state: QuestionDeliveryState
|
|
307
|
+
/** Null means a frozen submission may or may not have crossed the service boundary. */
|
|
308
|
+
submitted: boolean | null
|
|
309
|
+
request_id: string | null
|
|
310
|
+
frozen_request_id: string | null
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export type QuestionStateLookup =
|
|
314
|
+
| { found: true; session_id: string; question: QuestionStateView }
|
|
315
|
+
| { found: false; ambiguous: boolean }
|
|
316
|
+
|
|
276
317
|
/** Keep immutable session grouping while replacing per-event Git location authoritatively. */
|
|
277
318
|
function sourceContextAtHookEvent(
|
|
278
319
|
source: SourceContextT | undefined,
|
|
@@ -355,6 +396,8 @@ export interface HookOutcome {
|
|
|
355
396
|
notes: string[]
|
|
356
397
|
/** Structured lifecycle detail that belongs on hook.end without user text. */
|
|
357
398
|
log?: Record<string, unknown>
|
|
399
|
+
/** An unpushed registration survived UserPromptSubmit and needs its own owner. */
|
|
400
|
+
settlementRequired?: boolean
|
|
358
401
|
}
|
|
359
402
|
|
|
360
403
|
/** An accepted continuation ready for whichever host owns the last meter. */
|
|
@@ -416,6 +459,8 @@ export interface EscalationWaiterOptions {
|
|
|
416
459
|
envelope: HookEnvelope
|
|
417
460
|
route: EscalationDeliveryRoute
|
|
418
461
|
processDeadlineAt?: number
|
|
462
|
+
/** Stop owns diagnostics; a pre-Stop recovery owner must not impersonate it. */
|
|
463
|
+
recordStop?: boolean
|
|
419
464
|
}
|
|
420
465
|
|
|
421
466
|
function sessionStatePath(sessionId: string, env: NodeJS.ProcessEnv): string {
|
|
@@ -500,14 +545,19 @@ export function findOwningSession(
|
|
|
500
545
|
const pendingMatch = pendingList(state).some(
|
|
501
546
|
(entry) => entry.question_id === id || entry.request_id === id,
|
|
502
547
|
)
|
|
503
|
-
const retiringMatch = (state.retiring ?? []).some(
|
|
548
|
+
const retiringMatch = (state.retiring ?? []).some(
|
|
549
|
+
(entry) => entry.question_id === id || entry.request_id === id,
|
|
550
|
+
)
|
|
551
|
+
const historyMatch = (state.question_history ?? []).some(
|
|
552
|
+
(entry) => entry.question_id === id || entry.request_id === id,
|
|
553
|
+
)
|
|
504
554
|
const acknowledgementMatch = (state.acknowledgement_due ?? []).some(
|
|
505
555
|
(entry) => entry.request_id === id,
|
|
506
556
|
)
|
|
507
557
|
const acceptedMatch = state.accepted?.answers.some(
|
|
508
558
|
({ pending }) => pending.question_id === id || pending.request_id === id,
|
|
509
559
|
) ?? false
|
|
510
|
-
if (pendingMatch || retiringMatch || acknowledgementMatch || acceptedMatch) {
|
|
560
|
+
if (pendingMatch || retiringMatch || historyMatch || acknowledgementMatch || acceptedMatch) {
|
|
511
561
|
matches.push(sessionId)
|
|
512
562
|
if (matches.length > 1) return { sessionId: null, ambiguous: true }
|
|
513
563
|
}
|
|
@@ -1452,14 +1502,24 @@ export function parkForRetirement(
|
|
|
1452
1502
|
updateSessionState(sessionId, env, (current) => {
|
|
1453
1503
|
const already = current.retiring ?? []
|
|
1454
1504
|
const existing = already.findIndex((entry) => entry.request_id === retirement.request_id)
|
|
1455
|
-
if (existing < 0)
|
|
1505
|
+
if (existing < 0) {
|
|
1506
|
+
return rememberQuestionState(
|
|
1507
|
+
{ ...current, retiring: [...already, retirement] },
|
|
1508
|
+
pending,
|
|
1509
|
+
state === 'answered' ? 'answered' : 'retired',
|
|
1510
|
+
)
|
|
1511
|
+
}
|
|
1456
1512
|
const retiring = [...already]
|
|
1457
1513
|
// An observed answer is final truth and upgrades an earlier supersession
|
|
1458
1514
|
// parked while the submission callback was racing a newer question.
|
|
1459
1515
|
if (retirement.state === 'answered' && retiring[existing]!.state !== 'answered') {
|
|
1460
1516
|
retiring[existing] = retirement
|
|
1461
1517
|
}
|
|
1462
|
-
return
|
|
1518
|
+
return rememberQuestionState(
|
|
1519
|
+
{ ...current, retiring },
|
|
1520
|
+
pending,
|
|
1521
|
+
state === 'answered' ? 'answered' : 'retired',
|
|
1522
|
+
)
|
|
1463
1523
|
})
|
|
1464
1524
|
}
|
|
1465
1525
|
|
|
@@ -1494,6 +1554,7 @@ function retiringQuestion(
|
|
|
1494
1554
|
}
|
|
1495
1555
|
const source = sourceContextAtHookEvent(pending.source, cwd)
|
|
1496
1556
|
return {
|
|
1557
|
+
...(pending.question_id !== undefined ? { question_id: pending.question_id } : {}),
|
|
1497
1558
|
request_id: requestId!,
|
|
1498
1559
|
collapse_key: collapseKey!,
|
|
1499
1560
|
device_ids: [...deviceIds!],
|
|
@@ -1821,6 +1882,144 @@ function pendingList(state: SessionState): PendingQuestion[] {
|
|
|
1821
1882
|
return Array.isArray(state.pending) ? state.pending : []
|
|
1822
1883
|
}
|
|
1823
1884
|
|
|
1885
|
+
const QUESTION_HISTORY_CAP = 50
|
|
1886
|
+
|
|
1887
|
+
type QuestionIdentity = Pick<PendingQuestion, 'question_id' | 'request_id' | 'submission'>
|
|
1888
|
+
|
|
1889
|
+
/** Keep a bounded, content-free map from local identity to its terminal state. */
|
|
1890
|
+
function rememberQuestionState(
|
|
1891
|
+
current: SessionState,
|
|
1892
|
+
question: QuestionIdentity,
|
|
1893
|
+
state: QuestionTerminalState,
|
|
1894
|
+
): SessionState {
|
|
1895
|
+
if (question.question_id === undefined) return current
|
|
1896
|
+
const existing = (current.question_history ?? []).find(
|
|
1897
|
+
(entry) => entry.question_id === question.question_id,
|
|
1898
|
+
)
|
|
1899
|
+
const terminalState =
|
|
1900
|
+
existing?.state === 'withdrawn' || existing?.state === 'answered'
|
|
1901
|
+
? existing.state
|
|
1902
|
+
: state === 'answered'
|
|
1903
|
+
? 'answered'
|
|
1904
|
+
: state
|
|
1905
|
+
const requestId = question.request_id ?? existing?.request_id
|
|
1906
|
+
const frozenRequestId =
|
|
1907
|
+
requestId === undefined
|
|
1908
|
+
? question.submission?.request_id ?? existing?.frozen_request_id
|
|
1909
|
+
: undefined
|
|
1910
|
+
const record: QuestionHistoryEntry = {
|
|
1911
|
+
question_id: question.question_id,
|
|
1912
|
+
state: terminalState,
|
|
1913
|
+
...(requestId === undefined ? {} : { request_id: requestId }),
|
|
1914
|
+
...(frozenRequestId === undefined ? {} : { frozen_request_id: frozenRequestId }),
|
|
1915
|
+
}
|
|
1916
|
+
const history = [
|
|
1917
|
+
...(current.question_history ?? []).filter(
|
|
1918
|
+
(entry) => entry.question_id !== question.question_id,
|
|
1919
|
+
),
|
|
1920
|
+
record,
|
|
1921
|
+
].slice(-QUESTION_HISTORY_CAP)
|
|
1922
|
+
return { ...current, question_history: history }
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
function pendingQuestionView(entry: PendingQuestion): QuestionStateView | null {
|
|
1926
|
+
if (entry.question_id === undefined) return null
|
|
1927
|
+
if (entry.request_id !== undefined) {
|
|
1928
|
+
return {
|
|
1929
|
+
question_id: entry.question_id,
|
|
1930
|
+
state: 'live',
|
|
1931
|
+
submitted: true,
|
|
1932
|
+
request_id: entry.request_id,
|
|
1933
|
+
frozen_request_id: null,
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
if (entry.submission !== undefined) {
|
|
1937
|
+
return {
|
|
1938
|
+
question_id: entry.question_id,
|
|
1939
|
+
state: 'frozen',
|
|
1940
|
+
submitted: null,
|
|
1941
|
+
request_id: null,
|
|
1942
|
+
frozen_request_id: entry.submission.request_id,
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
return {
|
|
1946
|
+
question_id: entry.question_id,
|
|
1947
|
+
state: 'local',
|
|
1948
|
+
submitted: false,
|
|
1949
|
+
request_id: null,
|
|
1950
|
+
frozen_request_id: null,
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
/** Read one stable local question identity without minting or submitting anything. */
|
|
1955
|
+
export function inspectQuestionState(
|
|
1956
|
+
questionId: string,
|
|
1957
|
+
env: NodeJS.ProcessEnv,
|
|
1958
|
+
): QuestionStateLookup {
|
|
1959
|
+
const owner = findOwningSession(questionId, env)
|
|
1960
|
+
if (owner.ambiguous || owner.sessionId === null) {
|
|
1961
|
+
return { found: false, ambiguous: owner.ambiguous }
|
|
1962
|
+
}
|
|
1963
|
+
const state = readSessionState(owner.sessionId, env)
|
|
1964
|
+
const pending = pendingList(state).find((entry) => entry.question_id === questionId)
|
|
1965
|
+
const pendingView = pending === undefined ? null : pendingQuestionView(pending)
|
|
1966
|
+
if (pendingView !== null) {
|
|
1967
|
+
return { found: true, session_id: owner.sessionId, question: pendingView }
|
|
1968
|
+
}
|
|
1969
|
+
const accepted = state.accepted?.answers.find(
|
|
1970
|
+
({ pending: entry }) => entry.question_id === questionId,
|
|
1971
|
+
)?.pending
|
|
1972
|
+
if (accepted?.question_id !== undefined) {
|
|
1973
|
+
return {
|
|
1974
|
+
found: true,
|
|
1975
|
+
session_id: owner.sessionId,
|
|
1976
|
+
question: {
|
|
1977
|
+
question_id: accepted.question_id,
|
|
1978
|
+
state: 'answered',
|
|
1979
|
+
submitted: true,
|
|
1980
|
+
request_id: accepted.request_id ?? null,
|
|
1981
|
+
frozen_request_id: null,
|
|
1982
|
+
},
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
const retiring = (state.retiring ?? []).find((entry) => entry.question_id === questionId)
|
|
1986
|
+
if (retiring?.question_id !== undefined) {
|
|
1987
|
+
return {
|
|
1988
|
+
found: true,
|
|
1989
|
+
session_id: owner.sessionId,
|
|
1990
|
+
question: {
|
|
1991
|
+
question_id: retiring.question_id,
|
|
1992
|
+
state: retiring.state === 'answered' ? 'answered' : 'retired',
|
|
1993
|
+
submitted: true,
|
|
1994
|
+
request_id: retiring.request_id,
|
|
1995
|
+
frozen_request_id: null,
|
|
1996
|
+
},
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
const history = (state.question_history ?? []).find(
|
|
2000
|
+
(entry) => entry.question_id === questionId,
|
|
2001
|
+
)
|
|
2002
|
+
if (history !== undefined) {
|
|
2003
|
+
return {
|
|
2004
|
+
found: true,
|
|
2005
|
+
session_id: owner.sessionId,
|
|
2006
|
+
question: {
|
|
2007
|
+
question_id: history.question_id,
|
|
2008
|
+
state: history.state,
|
|
2009
|
+
submitted:
|
|
2010
|
+
history.request_id !== undefined
|
|
2011
|
+
? true
|
|
2012
|
+
: history.frozen_request_id !== undefined
|
|
2013
|
+
? null
|
|
2014
|
+
: false,
|
|
2015
|
+
request_id: history.request_id ?? null,
|
|
2016
|
+
frozen_request_id: history.frozen_request_id ?? null,
|
|
2017
|
+
},
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
return { found: false, ambiguous: false }
|
|
2021
|
+
}
|
|
2022
|
+
|
|
1824
2023
|
/** Registration identity — the convention every racing writer compares by. */
|
|
1825
2024
|
function isSamePending(a: PendingQuestion, b: PendingQuestion): boolean {
|
|
1826
2025
|
if (a.question_id !== undefined || b.question_id !== undefined) {
|
|
@@ -1842,13 +2041,14 @@ export function dropPendingQuestion(
|
|
|
1842
2041
|
sessionId: string,
|
|
1843
2042
|
env: NodeJS.ProcessEnv,
|
|
1844
2043
|
entry: PendingQuestion,
|
|
2044
|
+
terminalState: QuestionTerminalState = 'retired',
|
|
1845
2045
|
): void {
|
|
1846
2046
|
updateSessionState(sessionId, env, (current) => {
|
|
1847
2047
|
const pending = pendingList(current).filter((candidate) => !isSamePending(candidate, entry))
|
|
1848
2048
|
const next: SessionState = { ...current }
|
|
1849
2049
|
if (pending.length > 0) next.pending = pending
|
|
1850
2050
|
else delete next.pending
|
|
1851
|
-
return next
|
|
2051
|
+
return rememberQuestionState(next, entry, terminalState)
|
|
1852
2052
|
})
|
|
1853
2053
|
}
|
|
1854
2054
|
|
|
@@ -1870,7 +2070,10 @@ export function withdrawUnpushedQuestions(
|
|
|
1870
2070
|
const next: SessionState = { ...current }
|
|
1871
2071
|
if (remaining.length > 0) next.pending = remaining
|
|
1872
2072
|
else delete next.pending
|
|
1873
|
-
return
|
|
2073
|
+
return withdrawn.reduce(
|
|
2074
|
+
(remembered, entry) => rememberQuestionState(remembered, entry, 'withdrawn'),
|
|
2075
|
+
next,
|
|
2076
|
+
)
|
|
1874
2077
|
})
|
|
1875
2078
|
return withdrawn
|
|
1876
2079
|
}
|
|
@@ -2058,7 +2261,10 @@ function stageAcceptedAnswers(
|
|
|
2058
2261
|
else delete next.pending
|
|
2059
2262
|
if (acknowledgementDue.length > 0) next.acknowledgement_due = acknowledgementDue
|
|
2060
2263
|
else delete next.acknowledgement_due
|
|
2061
|
-
return
|
|
2264
|
+
return answered.reduce(
|
|
2265
|
+
(remembered, answer) => rememberQuestionState(remembered, answer.pending, 'answered'),
|
|
2266
|
+
next,
|
|
2267
|
+
)
|
|
2062
2268
|
})
|
|
2063
2269
|
return accepted
|
|
2064
2270
|
}
|
|
@@ -2296,7 +2502,14 @@ async function retirePendings(
|
|
|
2296
2502
|
else delete next.pending
|
|
2297
2503
|
if (retiring.length > 0) next.retiring = retiring
|
|
2298
2504
|
else delete next.retiring
|
|
2299
|
-
return
|
|
2505
|
+
return entries.reduce(
|
|
2506
|
+
(remembered, entry) => rememberQuestionState(
|
|
2507
|
+
remembered,
|
|
2508
|
+
entry,
|
|
2509
|
+
state === 'answered' ? 'answered' : 'retired',
|
|
2510
|
+
),
|
|
2511
|
+
next,
|
|
2512
|
+
)
|
|
2300
2513
|
})
|
|
2301
2514
|
// Network retirement is deliberately deferred to a later no-question hook.
|
|
2302
2515
|
// The state write above is the durable operation; blocking here creates a
|
|
@@ -2399,7 +2612,7 @@ export async function handleUserPromptSubmit(
|
|
|
2399
2612
|
// Park before dropping `pending`. If the process dies between these writes,
|
|
2400
2613
|
// the next hook sees both copies and dedupes them; the old order could die in
|
|
2401
2614
|
// the gap after erasing the only request/collapse/device identifiers.
|
|
2402
|
-
updateSessionState(sessionId, ctx.env, (current) => {
|
|
2615
|
+
const updated = updateSessionState(sessionId, ctx.env, (current) => {
|
|
2403
2616
|
const retiring = [...(current.retiring ?? [])]
|
|
2404
2617
|
const matchedNow = pendingAnsweredByPrompt(envelope.prompt, pendingList(current))
|
|
2405
2618
|
const unmatched = pendingList(current).filter(
|
|
@@ -2438,7 +2651,14 @@ export async function handleUserPromptSubmit(
|
|
|
2438
2651
|
if (current.continuation !== undefined) {
|
|
2439
2652
|
next.continuation = { ...current.continuation, count: 0 }
|
|
2440
2653
|
}
|
|
2441
|
-
return
|
|
2654
|
+
return matchedNow.reduce(
|
|
2655
|
+
(remembered, entry) => rememberQuestionState(
|
|
2656
|
+
remembered,
|
|
2657
|
+
entry,
|
|
2658
|
+
entry.request_id === undefined ? 'withdrawn' : 'retired',
|
|
2659
|
+
),
|
|
2660
|
+
next,
|
|
2661
|
+
)
|
|
2442
2662
|
})
|
|
2443
2663
|
// The bridge that lets a plain `notifai ask` find the hook's canonical
|
|
2444
2664
|
// session: an agent shell command gets no hook payload, and not every
|
|
@@ -2452,7 +2672,10 @@ export async function handleUserPromptSubmit(
|
|
|
2452
2672
|
// model. Keep the journal and let the next Stop use its acknowledged
|
|
2453
2673
|
// continuation channel instead of recreating the crash-before-stdout gap.
|
|
2454
2674
|
notes.push('the late device answer will continue the agent at this turn’s Stop')
|
|
2455
|
-
return {
|
|
2675
|
+
return {
|
|
2676
|
+
notes,
|
|
2677
|
+
settlementRequired: pendingList(updated).some((entry) => entry.request_id === undefined),
|
|
2678
|
+
}
|
|
2456
2679
|
}
|
|
2457
2680
|
const retired = await drainRetirements(ctx, sessionId, ctx.env)
|
|
2458
2681
|
const orphaned = await drainOrphanRetirements(ctx, ctx.env, ctx.now())
|
|
@@ -2460,7 +2683,10 @@ export async function handleUserPromptSubmit(
|
|
|
2460
2683
|
if (swept.length > 0) {
|
|
2461
2684
|
notes.push(`retired question${swept.length > 1 ? 's' : ''} ${swept.join(', ')}`)
|
|
2462
2685
|
}
|
|
2463
|
-
return {
|
|
2686
|
+
return {
|
|
2687
|
+
notes,
|
|
2688
|
+
settlementRequired: pendingList(updated).some((entry) => entry.request_id === undefined),
|
|
2689
|
+
}
|
|
2464
2690
|
}
|
|
2465
2691
|
|
|
2466
2692
|
// ---------------------------------------------------------------------------
|
|
@@ -2477,6 +2703,7 @@ export async function handleStop(
|
|
|
2477
2703
|
envelope: HookEnvelope,
|
|
2478
2704
|
processDeadlineAt = ctx.now() + QUESTION_WAITER_CEILING_SECONDS * 1000,
|
|
2479
2705
|
route: EscalationDeliveryRoute = hookContinuationRoute(),
|
|
2706
|
+
recordStop = true,
|
|
2480
2707
|
): Promise<HookOutcome> {
|
|
2481
2708
|
const sessionId = envelope.session_id
|
|
2482
2709
|
if (!sessionId) {
|
|
@@ -2488,6 +2715,7 @@ export async function handleStop(
|
|
|
2488
2715
|
envelope,
|
|
2489
2716
|
route,
|
|
2490
2717
|
processDeadlineAt,
|
|
2718
|
+
recordStop,
|
|
2491
2719
|
})
|
|
2492
2720
|
}
|
|
2493
2721
|
|
|
@@ -2545,11 +2773,13 @@ export async function runEscalationWaiter(
|
|
|
2545
2773
|
// This event marker is diagnostic evidence in its own right. Record it
|
|
2546
2774
|
// before every early return so doctor can distinguish a working Stop route
|
|
2547
2775
|
// from the prompt-only state that previously looked healthy.
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2776
|
+
if (options.recordStop !== false) {
|
|
2777
|
+
updateSessionState(sessionId, ctx.env, (current) => ({
|
|
2778
|
+
...current,
|
|
2779
|
+
...(ctx.harness === undefined ? {} : { harness: ctx.harness }),
|
|
2780
|
+
last_stop_at: ctx.now(),
|
|
2781
|
+
}))
|
|
2782
|
+
}
|
|
2553
2783
|
|
|
2554
2784
|
// The waiter, not its host, owns this lease. It spans grace, submission,
|
|
2555
2785
|
// polling, close fencing, route delivery, and every retirement path.
|
|
@@ -3600,6 +3830,24 @@ export function handleSessionEnd(
|
|
|
3600
3830
|
if (envelope.cwd !== undefined) clearMatchingProjectSession(envelope.cwd, env, sessionId)
|
|
3601
3831
|
|
|
3602
3832
|
const state = readSessionState(sessionId, env)
|
|
3833
|
+
let stateWithHistory = state
|
|
3834
|
+
for (const entry of pendingList(state)) {
|
|
3835
|
+
stateWithHistory = rememberQuestionState(
|
|
3836
|
+
stateWithHistory,
|
|
3837
|
+
entry,
|
|
3838
|
+
entry.request_id === undefined && entry.submission === undefined ? 'withdrawn' : 'retired',
|
|
3839
|
+
)
|
|
3840
|
+
}
|
|
3841
|
+
for (const answer of state.accepted?.answers ?? []) {
|
|
3842
|
+
stateWithHistory = rememberQuestionState(stateWithHistory, answer.pending, 'answered')
|
|
3843
|
+
}
|
|
3844
|
+
for (const retirement of state.retiring ?? []) {
|
|
3845
|
+
stateWithHistory = rememberQuestionState(
|
|
3846
|
+
stateWithHistory,
|
|
3847
|
+
retirement,
|
|
3848
|
+
retirement.state === 'answered' ? 'answered' : 'retired',
|
|
3849
|
+
)
|
|
3850
|
+
}
|
|
3603
3851
|
const orphans: RetiringQuestion[] = [...(state.retiring ?? [])]
|
|
3604
3852
|
const retirementCandidates = [
|
|
3605
3853
|
...pendingList(state),
|
|
@@ -3631,7 +3879,7 @@ export function handleSessionEnd(
|
|
|
3631
3879
|
)
|
|
3632
3880
|
}
|
|
3633
3881
|
if (state.accepted !== undefined || (state.acknowledgement_due?.length ?? 0) > 0) {
|
|
3634
|
-
const preserved: SessionState = { ...
|
|
3882
|
+
const preserved: SessionState = { ...stateWithHistory }
|
|
3635
3883
|
delete preserved.pending
|
|
3636
3884
|
delete preserved.retiring
|
|
3637
3885
|
delete preserved.acknowledgement_blocks
|
|
@@ -3654,7 +3902,11 @@ export function handleSessionEnd(
|
|
|
3654
3902
|
},
|
|
3655
3903
|
}
|
|
3656
3904
|
}
|
|
3905
|
+
const history = stateWithHistory.question_history
|
|
3657
3906
|
clearSessionState(sessionId, env)
|
|
3907
|
+
if ((history?.length ?? 0) > 0) {
|
|
3908
|
+
writeSessionState(sessionId, env, { question_history: history! })
|
|
3909
|
+
}
|
|
3658
3910
|
return { notes, log: { outcome: 'cleaned', queued_retirements: orphans.length } }
|
|
3659
3911
|
}
|
|
3660
3912
|
|