@gajae-code/coding-agent 0.16.0 → 0.16.3
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 +52 -1
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/model-registry.d.ts +16 -0
- package/dist/types/config/settings-schema.d.ts +17 -7
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -1
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +17 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +3 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/rows.d.ts +15 -2
- package/dist/types/sdk/cli/session-cli.d.ts +2 -1
- package/dist/types/sdk/host/events.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +18 -17
- package/dist/types/sdk/lifecycle-session.d.ts +1 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +4 -0
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +47 -7
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +14 -2
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +12 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/index.d.ts +4 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +12 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +7 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/update-cli.ts +63 -12
- package/src/commands/sdk.ts +22 -7
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/model-preset-registry.ts +177 -18
- package/src/config/model-registry.ts +268 -42
- package/src/config/settings-schema.ts +19 -7
- package/src/config/settings.ts +46 -15
- package/src/coordinator-mcp/projection-scan.ts +6 -6
- package/src/coordinator-mcp/server.ts +21 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/session-state-lock.ts +156 -32
- package/src/gjc-runtime/session-state-sidecar.ts +10 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +34 -19
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/selector-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +6 -1
- package/src/sdk/broker/broker.ts +10 -1
- package/src/sdk/broker/ensure.ts +76 -3
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +254 -34
- package/src/sdk/broker/spawn-substrate.ts +45 -2
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +17 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +118 -24
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +33 -3
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/rows.ts +75 -7
- package/src/sdk/cli/session-cli.ts +68 -35
- package/src/sdk/host/control/dispatch.ts +2 -2
- package/src/sdk/host/events.ts +26 -2
- package/src/sdk/host/host.ts +6 -1
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +497 -31
- package/src/sdk/host/session-runtime.ts +387 -54
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle-session.ts +7 -2
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +54 -3
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +210 -94
- package/src/sdk/session.ts +59 -41
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1033 -260
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/session-manager.ts +517 -77
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +158 -33
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/index.ts +4 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +86 -1
|
@@ -14,7 +14,7 @@ import * as fs from "node:fs/promises";
|
|
|
14
14
|
import * as path from "node:path";
|
|
15
15
|
import type { PromptReconciliationStatus, SdkPromptTerminalOutcome } from "../prompt-status";
|
|
16
16
|
import type { ReceiptState } from "../receipt-state";
|
|
17
|
-
import { TURN_RESULT_CONTENT_MAX_BYTES, type TurnResultContent } from "../turn-result";
|
|
17
|
+
import { reportableTurnResultContent, TURN_RESULT_CONTENT_MAX_BYTES, type TurnResultContent } from "../turn-result";
|
|
18
18
|
import type { PromptCorrelation } from "./prompt-reconciliation";
|
|
19
19
|
|
|
20
20
|
export const RECONCILIATION_STORE_VERSION = 2;
|
|
@@ -25,6 +25,16 @@ export const RECONCILIATION_DIR_NAME = ".sdk-reconciliation";
|
|
|
25
25
|
// durable document can never match a real retry and would make the reserved key
|
|
26
26
|
// invisible (and reusable for an unrelated later abort), so reload rejects it.
|
|
27
27
|
const SHA256_RE = /^[0-9a-f]{64}$/;
|
|
28
|
+
const SAFE_FAILURE_CODE_RE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
29
|
+
const DURABLE_FAILURE_MESSAGE_MAX_BYTES = 4_096;
|
|
30
|
+
|
|
31
|
+
function isSafeFailureDiagnostic(value: unknown): value is { code: string; message: string } {
|
|
32
|
+
if (!isRecord(value) || typeof value.code !== "string" || !SAFE_FAILURE_CODE_RE.test(value.code)) return false;
|
|
33
|
+
return (
|
|
34
|
+
typeof value.message === "string" &&
|
|
35
|
+
new TextEncoder().encode(value.message).length <= DURABLE_FAILURE_MESSAGE_MAX_BYTES
|
|
36
|
+
);
|
|
37
|
+
}
|
|
28
38
|
|
|
29
39
|
export function resolveReconciliationSessionFile(
|
|
30
40
|
sessionFile: string | undefined | null,
|
|
@@ -207,11 +217,7 @@ function isValidRecord(value: unknown): boolean {
|
|
|
207
217
|
const settled = value.status !== "dispatching";
|
|
208
218
|
if (settled !== (typeof value.settledAt === "number" && Number.isFinite(value.settledAt))) return false;
|
|
209
219
|
if ((value.status === "rejected" || value.status === "uncertain") !== (value.error !== undefined)) return false;
|
|
210
|
-
if (
|
|
211
|
-
value.error !== undefined &&
|
|
212
|
-
(!isRecord(value.error) || typeof value.error.code !== "string" || typeof value.error.message !== "string")
|
|
213
|
-
)
|
|
214
|
-
return false;
|
|
220
|
+
if (value.error !== undefined && !isSafeFailureDiagnostic(value.error)) return false;
|
|
215
221
|
return value.receiptState === undefined && value.outcome === undefined;
|
|
216
222
|
}
|
|
217
223
|
if (kind !== "prompt" && kind !== "skill") return false;
|
|
@@ -274,8 +280,7 @@ function isValidRecord(value: unknown): boolean {
|
|
|
274
280
|
return false;
|
|
275
281
|
}
|
|
276
282
|
if (value.error !== undefined) {
|
|
277
|
-
if (!
|
|
278
|
-
if (typeof value.error.code !== "string" || typeof value.error.message !== "string") return false;
|
|
283
|
+
if (!isSafeFailureDiagnostic(value.error)) return false;
|
|
279
284
|
}
|
|
280
285
|
return [outcome, pendingOutcome].every(candidate => {
|
|
281
286
|
if (candidate === undefined) return true;
|
|
@@ -286,12 +291,13 @@ function isValidRecord(value: unknown): boolean {
|
|
|
286
291
|
candidate.reason as string,
|
|
287
292
|
) && ["agent", "client_cancel"].includes(candidate.provenance as string)
|
|
288
293
|
);
|
|
289
|
-
|
|
290
|
-
candidate.kind
|
|
291
|
-
["prompt_failed", "prompt_deadline_exceeded"].includes(candidate.code as string)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
294
|
+
if (
|
|
295
|
+
candidate.kind !== "failed" ||
|
|
296
|
+
!["prompt_failed", "prompt_deadline_exceeded"].includes(candidate.code as string) ||
|
|
297
|
+
!["agent_failed", "deadline"].includes(candidate.provenance as string)
|
|
298
|
+
)
|
|
299
|
+
return false;
|
|
300
|
+
return isSafeFailureDiagnostic({ code: candidate.code, message: candidate.message });
|
|
295
301
|
});
|
|
296
302
|
}
|
|
297
303
|
/** Terminal scope validation: bounded origin/fence/settlement fields only. */
|
|
@@ -361,8 +367,9 @@ function isValidTerminalScope(value: unknown): boolean {
|
|
|
361
367
|
responseState !== "failed"
|
|
362
368
|
)
|
|
363
369
|
return false;
|
|
364
|
-
if (typeof responsePayloadHash !== "string" || !responsePayloadHash) return false;
|
|
365
|
-
if (replayPayloadHash !== undefined && (typeof replayPayloadHash !== "string" || !replayPayloadHash))
|
|
370
|
+
if (typeof responsePayloadHash !== "string" || !SHA256_RE.test(responsePayloadHash)) return false;
|
|
371
|
+
if (replayPayloadHash !== undefined && (typeof replayPayloadHash !== "string" || !SHA256_RE.test(replayPayloadHash)))
|
|
372
|
+
return false;
|
|
366
373
|
if (typeof idempotencyKeyHash !== "string" || !SHA256_RE.test(idempotencyKeyHash)) return false;
|
|
367
374
|
if (typeof idempotencyInputHash !== "string" || !SHA256_RE.test(idempotencyInputHash)) return false;
|
|
368
375
|
if (terminalPublished !== undefined && typeof terminalPublished !== "boolean") return false;
|
|
@@ -406,12 +413,12 @@ function isValidEvictedTerminalKeyEntry(value: unknown): value is EvictedTermina
|
|
|
406
413
|
return false;
|
|
407
414
|
if (
|
|
408
415
|
value.responsePayloadHash !== undefined &&
|
|
409
|
-
(typeof value.responsePayloadHash !== "string" || !value.responsePayloadHash)
|
|
416
|
+
(typeof value.responsePayloadHash !== "string" || !SHA256_RE.test(value.responsePayloadHash))
|
|
410
417
|
)
|
|
411
418
|
return false;
|
|
412
419
|
if (
|
|
413
420
|
value.replayPayloadHash !== undefined &&
|
|
414
|
-
(typeof value.replayPayloadHash !== "string" || !value.replayPayloadHash)
|
|
421
|
+
(typeof value.replayPayloadHash !== "string" || !SHA256_RE.test(value.replayPayloadHash))
|
|
415
422
|
)
|
|
416
423
|
return false;
|
|
417
424
|
if (value.terminalPublished !== undefined && typeof value.terminalPublished !== "boolean") return false;
|
|
@@ -550,6 +557,14 @@ export function settleProcessRestart(
|
|
|
550
557
|
records: DurableReconciliationRecord[],
|
|
551
558
|
now: number,
|
|
552
559
|
): DurableReconciliationRecord[] {
|
|
560
|
+
const isDeadlineOutcome = (outcome: SdkPromptTerminalOutcome | undefined): boolean =>
|
|
561
|
+
outcome?.kind === "failed" && outcome.provenance === "deadline";
|
|
562
|
+
const providerFailureOutcome = (error: { code: string; message: string }): SdkPromptTerminalOutcome => ({
|
|
563
|
+
kind: "failed",
|
|
564
|
+
code: "prompt_failed",
|
|
565
|
+
message: error.message,
|
|
566
|
+
provenance: "agent_failed",
|
|
567
|
+
});
|
|
553
568
|
return records.map(record => {
|
|
554
569
|
if (record.kind === "steer") {
|
|
555
570
|
if (record.status !== "dispatching") return record;
|
|
@@ -569,29 +584,38 @@ export function settleProcessRestart(
|
|
|
569
584
|
return record;
|
|
570
585
|
}
|
|
571
586
|
if (record.pendingOutcome !== undefined || record.kind === "prompt") {
|
|
587
|
+
const { pendingOutcome, pendingReceiptState, ...settledRecord } = record;
|
|
588
|
+
const providerErrorRecord = record.error;
|
|
589
|
+
const providerError =
|
|
590
|
+
providerErrorRecord !== undefined && providerErrorRecord.code !== "prompt_deadline_exceeded";
|
|
591
|
+
const preserveProviderError = providerError;
|
|
572
592
|
const outcome: SdkPromptTerminalOutcome =
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
message: record.error.message,
|
|
578
|
-
provenance: "agent_failed",
|
|
579
|
-
}
|
|
580
|
-
: (record.pendingOutcome ?? {
|
|
593
|
+
providerError &&
|
|
594
|
+
(pendingOutcome === undefined || isDeadlineOutcome(pendingOutcome) || pendingOutcome.kind === "stopped")
|
|
595
|
+
? providerFailureOutcome(providerErrorRecord)
|
|
596
|
+
: (pendingOutcome ?? {
|
|
581
597
|
kind: "failed",
|
|
582
598
|
code: "prompt_failed",
|
|
583
599
|
message: "Prompt did not complete before process restart.",
|
|
584
600
|
provenance: "agent_failed",
|
|
585
601
|
});
|
|
602
|
+
const preservedError =
|
|
603
|
+
providerError && preserveProviderError
|
|
604
|
+
? providerErrorRecord
|
|
605
|
+
: outcome.kind === "failed"
|
|
606
|
+
? { code: outcome.code, message: outcome.message }
|
|
607
|
+
: undefined;
|
|
608
|
+
const receiptState = reportableTurnResultContent(record.content)
|
|
609
|
+
? "present"
|
|
610
|
+
: (pendingReceiptState ?? (outcome.kind === "stopped" ? "missing" : "unknown"));
|
|
586
611
|
return {
|
|
587
|
-
...
|
|
612
|
+
...settledRecord,
|
|
588
613
|
status: outcome.kind === "stopped" ? "terminal_ok" : "failed",
|
|
589
614
|
terminalAt: now,
|
|
590
615
|
outcome,
|
|
591
|
-
receiptState
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
...(outcome.kind === "failed" ? { error: { code: outcome.code, message: outcome.message } } : {}),
|
|
616
|
+
receiptState,
|
|
617
|
+
|
|
618
|
+
...(preservedError !== undefined ? { error: preservedError } : { error: undefined }),
|
|
595
619
|
};
|
|
596
620
|
}
|
|
597
621
|
return {
|
|
@@ -6,7 +6,7 @@ export interface SlackInboundDispatchReceipt {
|
|
|
6
6
|
interactionId: string;
|
|
7
7
|
retryKey: string;
|
|
8
8
|
eventContext?: string;
|
|
9
|
-
kind: "action" | "command";
|
|
9
|
+
kind: "action" | "command" | "message";
|
|
10
10
|
actionId?: string;
|
|
11
11
|
/** The endpoint generation and SDK effect captured before Socket Mode ACK. */
|
|
12
12
|
endpointGeneration: number;
|
|
@@ -145,13 +145,14 @@ type SlackInboundRouting = {
|
|
|
145
145
|
interactionId: string;
|
|
146
146
|
retryKey: string;
|
|
147
147
|
eventContext?: string;
|
|
148
|
-
kind: "action" | "command";
|
|
148
|
+
kind: "action" | "command" | "message";
|
|
149
149
|
actionId?: string;
|
|
150
150
|
};
|
|
151
151
|
|
|
152
152
|
type SlackInboundEffectPayload =
|
|
153
153
|
| { type: "reply"; id: string; answer: string; idempotencyKey: string; routing: SlackInboundRouting }
|
|
154
|
-
| { type: "command"; content: string; idempotencyKey: string; routing: SlackInboundRouting }
|
|
154
|
+
| { type: "command"; content: string; idempotencyKey: string; routing: SlackInboundRouting }
|
|
155
|
+
| { type: "user_message"; sessionId: string; text: string; idempotencyKey: string; routing: SlackInboundRouting };
|
|
155
156
|
|
|
156
157
|
type SlackRootPublication = {
|
|
157
158
|
conversation: SlackConversation;
|
|
@@ -169,7 +170,11 @@ function messageFromEnvelope(
|
|
|
169
170
|
| undefined {
|
|
170
171
|
if (!payload || typeof payload !== "object") return undefined;
|
|
171
172
|
const eventPayload = payload as EventsPayload;
|
|
172
|
-
if (
|
|
173
|
+
if (
|
|
174
|
+
(eventPayload.type !== "events_api" && eventPayload.type !== "event_callback") ||
|
|
175
|
+
!eventPayload.event ||
|
|
176
|
+
eventPayload.event.type !== "message"
|
|
177
|
+
)
|
|
173
178
|
return undefined;
|
|
174
179
|
const eventId = text(eventPayload.event_id);
|
|
175
180
|
const teamId = text(eventPayload.team_id);
|
|
@@ -191,6 +196,10 @@ function nextRecord(record: SlackConversation, update: Partial<SlackConversation
|
|
|
191
196
|
return normalizeSlackConversation({ ...record, ...update, generation: record.generation + 1 });
|
|
192
197
|
}
|
|
193
198
|
|
|
199
|
+
function replyableActionId(actionId: string | undefined): string | undefined {
|
|
200
|
+
return actionId?.startsWith("idle:") ? undefined : actionId;
|
|
201
|
+
}
|
|
202
|
+
|
|
194
203
|
/**
|
|
195
204
|
* Slack Socket Mode notification daemon. Accepted inbound SDK effects are
|
|
196
205
|
* persisted before acknowledgement, including their replay payload and captured
|
|
@@ -449,7 +458,7 @@ export class SlackNotificationDaemon {
|
|
|
449
458
|
const actorId = text(inbound.event.user);
|
|
450
459
|
if (
|
|
451
460
|
inbound.event.bot_id ||
|
|
452
|
-
inbound.event.subtype
|
|
461
|
+
inbound.event.subtype !== undefined ||
|
|
453
462
|
actorId === this.options.botUserId ||
|
|
454
463
|
!actorId ||
|
|
455
464
|
!(await this.#actorAuthorized(actorId))
|
|
@@ -852,7 +861,7 @@ export class SlackNotificationDaemon {
|
|
|
852
861
|
publicationId === undefined
|
|
853
862
|
? undefined
|
|
854
863
|
: await this.#publicationAttempt(
|
|
855
|
-
`root:${sessionId}:${publicationId}`,
|
|
864
|
+
`root:${this.options.teamId}:${this.options.channelId}:${sessionId}:${publicationId}`,
|
|
856
865
|
clientMsgId => `root:${sessionId}:${clientMsgId}`,
|
|
857
866
|
);
|
|
858
867
|
const existing = await this.findSession(sessionId, false);
|
|
@@ -1039,16 +1048,21 @@ export class SlackNotificationDaemon {
|
|
|
1039
1048
|
return await this.#track(this.#close(sessionId, marker, true, endpointGeneration));
|
|
1040
1049
|
}
|
|
1041
1050
|
|
|
1042
|
-
async recoverCleanup(sessionId: string, endpointGeneration: number): Promise<
|
|
1051
|
+
async recoverCleanup(sessionId: string, endpointGeneration: number, authorityId?: string): Promise<boolean> {
|
|
1043
1052
|
const found = await this.findSession(sessionId, true);
|
|
1053
|
+
if (!found) return true;
|
|
1054
|
+
if (found.record.endpointGeneration !== endpointGeneration) return true;
|
|
1055
|
+
if (found.record.state === "closed_marker" || found.record.state === "error") return true;
|
|
1044
1056
|
if (
|
|
1045
|
-
|
|
1046
|
-
found.record.
|
|
1047
|
-
found.record.endpointGeneration !== endpointGeneration
|
|
1057
|
+
(found.record.state === "posting_root" || found.record.state === "resumed_root") &&
|
|
1058
|
+
found.record.attachmentAuthorityId === authorityId
|
|
1048
1059
|
)
|
|
1049
|
-
return;
|
|
1060
|
+
return true;
|
|
1061
|
+
if (found.record.state !== "active") return false;
|
|
1062
|
+
if (!found.record.rootTs) return false;
|
|
1063
|
+
if (found.record.attachmentAuthorityId === authorityId && found.record.cleanupEffectId === undefined) return true;
|
|
1050
1064
|
const effectId = `close-marker-cleanup:${sessionId}:${found.record.clientMsgId ?? found.record.rootTs}`;
|
|
1051
|
-
if (found.record.cleanupEffectId !== effectId) return;
|
|
1065
|
+
if (found.record.cleanupEffectId !== effectId) return false;
|
|
1052
1066
|
const effect = await this.#journal.read(effectId);
|
|
1053
1067
|
if (!effect || (effect.state === "terminal" && !effect.receipt?.messageId && !effect.receipt?.timestamp)) {
|
|
1054
1068
|
await this.store.transact(found.key, current =>
|
|
@@ -1056,9 +1070,10 @@ export class SlackNotificationDaemon {
|
|
|
1056
1070
|
? nextRecord(current, { cleanupEffectId: undefined, updatedAt: this.#now() })
|
|
1057
1071
|
: current,
|
|
1058
1072
|
);
|
|
1059
|
-
return;
|
|
1073
|
+
return false;
|
|
1060
1074
|
}
|
|
1061
1075
|
await this.close(sessionId, "Session closed.", endpointGeneration);
|
|
1076
|
+
return true;
|
|
1062
1077
|
}
|
|
1063
1078
|
|
|
1064
1079
|
async retireAttachment(sessionId: string, endpointGeneration: number): Promise<void> {
|
|
@@ -1209,7 +1224,7 @@ export class SlackNotificationDaemon {
|
|
|
1209
1224
|
publicationId === undefined
|
|
1210
1225
|
? undefined
|
|
1211
1226
|
: await this.#publicationAttempt(
|
|
1212
|
-
`root:${sessionId}:${publicationId}`,
|
|
1227
|
+
`root:${this.options.teamId}:${this.options.channelId}:${sessionId}:${publicationId}`,
|
|
1213
1228
|
clientMsgId => `root:${sessionId}:${clientMsgId}`,
|
|
1214
1229
|
);
|
|
1215
1230
|
return (await this.#resumeWithRootPublication(sessionId, body, endpointGeneration, rootPublication?.clientMsgId))
|
|
@@ -1389,7 +1404,10 @@ export class SlackNotificationDaemon {
|
|
|
1389
1404
|
const interactionId = text(inbound.event.client_msg_id) ?? inbound.eventId;
|
|
1390
1405
|
const retryKey = `${inbound.eventId}:${interactionId}`;
|
|
1391
1406
|
const inboundText = text(inbound.event.text);
|
|
1392
|
-
|
|
1407
|
+
if (!inboundText) return undefined;
|
|
1408
|
+
const command = inboundText.startsWith("/sdk ");
|
|
1409
|
+
const actionId = command ? undefined : replyableActionId(record.pendingActionId);
|
|
1410
|
+
const kind: SlackInboundRouting["kind"] = command ? "command" : actionId ? "action" : "message";
|
|
1393
1411
|
|
|
1394
1412
|
const idempotencyKey = `slack:${inbound.teamId}:${inbound.channelId}:${inbound.rootTs}:${actorId}:${inbound.eventId}:${interactionId}`;
|
|
1395
1413
|
const effectId = `inbound:${inbound.teamId}:${inbound.channelId}:${inbound.rootTs}:${actorId}:${inbound.eventId}:${interactionId}`;
|
|
@@ -1403,21 +1421,26 @@ export class SlackNotificationDaemon {
|
|
|
1403
1421
|
actorId,
|
|
1404
1422
|
retryKey,
|
|
1405
1423
|
eventContext: inbound.eventContext,
|
|
1406
|
-
kind
|
|
1407
|
-
...(
|
|
1424
|
+
kind,
|
|
1425
|
+
...(actionId ? { actionId } : {}),
|
|
1408
1426
|
};
|
|
1409
|
-
const initialPayload: SlackInboundEffectPayload
|
|
1410
|
-
? { type: "command", content: inboundText
|
|
1411
|
-
:
|
|
1412
|
-
? { type: "reply", id:
|
|
1413
|
-
:
|
|
1427
|
+
const initialPayload: SlackInboundEffectPayload = command
|
|
1428
|
+
? { type: "command", content: inboundText, idempotencyKey, routing }
|
|
1429
|
+
: actionId
|
|
1430
|
+
? { type: "reply", id: actionId, answer: inboundText, idempotencyKey, routing }
|
|
1431
|
+
: { type: "user_message", sessionId: record.sessionId, text: inboundText, idempotencyKey, routing };
|
|
1414
1432
|
const existingEffect = await this.#journal.read<SlackInboundEffectPayload>(effectId);
|
|
1415
1433
|
const payload = existingEffect?.payload ?? initialPayload;
|
|
1416
|
-
|
|
1434
|
+
const dispatchKind = payload.routing.kind;
|
|
1417
1435
|
await this.#rescheduleAfterEffectTransition(
|
|
1418
1436
|
this.#journal.enqueue({
|
|
1419
1437
|
id: effectId,
|
|
1420
|
-
kind:
|
|
1438
|
+
kind:
|
|
1439
|
+
dispatchKind === "command"
|
|
1440
|
+
? "sdk.inbound.command"
|
|
1441
|
+
: dispatchKind === "action"
|
|
1442
|
+
? "sdk.inbound.reply"
|
|
1443
|
+
: "sdk.inbound.user_message",
|
|
1421
1444
|
transport: "slack",
|
|
1422
1445
|
sessionId: record.sessionId,
|
|
1423
1446
|
endpointGeneration: endpoint.generation,
|
|
@@ -1437,8 +1460,7 @@ export class SlackNotificationDaemon {
|
|
|
1437
1460
|
candidate =>
|
|
1438
1461
|
candidate.eventId === inbound.eventId ||
|
|
1439
1462
|
candidate.interactionId === interactionId ||
|
|
1440
|
-
candidate.retryKey === retryKey
|
|
1441
|
-
(inbound.eventContext !== undefined && candidate.eventContext === inbound.eventContext),
|
|
1463
|
+
candidate.retryKey === retryKey,
|
|
1442
1464
|
);
|
|
1443
1465
|
if (existing) {
|
|
1444
1466
|
sessionId = current.sessionId;
|
|
@@ -1449,8 +1471,8 @@ export class SlackNotificationDaemon {
|
|
|
1449
1471
|
current.seenEventIds.includes(inbound.eventId) ||
|
|
1450
1472
|
current.seenInteractionIds.includes(interactionId) ||
|
|
1451
1473
|
current.seenRetryKeys.includes(retryKey) ||
|
|
1452
|
-
(
|
|
1453
|
-
(
|
|
1474
|
+
(dispatchKind === "action" && replyableActionId(current.pendingActionId) !== payload.routing.actionId) ||
|
|
1475
|
+
(dispatchKind === "message" && replyableActionId(current.pendingActionId) !== undefined)
|
|
1454
1476
|
)
|
|
1455
1477
|
return current;
|
|
1456
1478
|
sessionId = current.sessionId;
|
|
@@ -1460,8 +1482,8 @@ export class SlackNotificationDaemon {
|
|
|
1460
1482
|
interactionId,
|
|
1461
1483
|
retryKey,
|
|
1462
1484
|
eventContext: inbound.eventContext,
|
|
1463
|
-
kind:
|
|
1464
|
-
...(
|
|
1485
|
+
kind: dispatchKind,
|
|
1486
|
+
...(dispatchKind === "action" ? { actionId: payload.routing.actionId } : {}),
|
|
1465
1487
|
endpointGeneration: endpoint.generation,
|
|
1466
1488
|
...(current.attachmentAuthorityId === undefined
|
|
1467
1489
|
? {}
|
|
@@ -1747,7 +1769,9 @@ export class SlackNotificationDaemon {
|
|
|
1747
1769
|
effect.transport !== "slack" ||
|
|
1748
1770
|
effect.state === "terminal" ||
|
|
1749
1771
|
!effect.sessionId ||
|
|
1750
|
-
(effect.kind !== "sdk.inbound.command" &&
|
|
1772
|
+
(effect.kind !== "sdk.inbound.command" &&
|
|
1773
|
+
effect.kind !== "sdk.inbound.reply" &&
|
|
1774
|
+
effect.kind !== "sdk.inbound.user_message")
|
|
1751
1775
|
)
|
|
1752
1776
|
continue;
|
|
1753
1777
|
const adopted = await this.#adoptOrphanInbound(effect as ChatEffect<SlackInboundEffectPayload>);
|
|
@@ -1794,7 +1818,9 @@ export class SlackNotificationDaemon {
|
|
|
1794
1818
|
text(routing.interactionId) !== undefined &&
|
|
1795
1819
|
routing.retryKey === `${routing.eventId}:${routing.interactionId}` &&
|
|
1796
1820
|
(routing.eventContext === undefined || text(routing.eventContext) !== undefined) &&
|
|
1797
|
-
(routing.kind === "command" ||
|
|
1821
|
+
(routing.kind === "command" ||
|
|
1822
|
+
routing.kind === "message" ||
|
|
1823
|
+
(routing.kind === "action" && text(routing.actionId) !== undefined))
|
|
1798
1824
|
);
|
|
1799
1825
|
}
|
|
1800
1826
|
#matchesInboundEffect(effect: ChatEffect<SlackInboundEffectPayload>, receipt: SlackInboundDispatchReceipt): boolean {
|
|
@@ -1828,7 +1854,11 @@ export class SlackNotificationDaemon {
|
|
|
1828
1854
|
(payload.type === "reply" &&
|
|
1829
1855
|
routing.kind === "action" &&
|
|
1830
1856
|
effect.kind === "sdk.inbound.reply" &&
|
|
1831
|
-
payload.id === routing.actionId)
|
|
1857
|
+
payload.id === routing.actionId) ||
|
|
1858
|
+
(payload.type === "user_message" &&
|
|
1859
|
+
routing.kind === "message" &&
|
|
1860
|
+
effect.kind === "sdk.inbound.user_message" &&
|
|
1861
|
+
payload.sessionId === effect.sessionId))
|
|
1832
1862
|
);
|
|
1833
1863
|
}
|
|
1834
1864
|
#sameInboundReceipt(left: SlackInboundDispatchReceipt, right: SlackInboundDispatchReceipt): boolean {
|
|
@@ -1868,7 +1898,9 @@ export class SlackNotificationDaemon {
|
|
|
1868
1898
|
if (
|
|
1869
1899
|
effect.transport !== "slack" ||
|
|
1870
1900
|
effect.state !== "terminal" ||
|
|
1871
|
-
(effect.kind !== "sdk.inbound.command" &&
|
|
1901
|
+
(effect.kind !== "sdk.inbound.command" &&
|
|
1902
|
+
effect.kind !== "sdk.inbound.reply" &&
|
|
1903
|
+
effect.kind !== "sdk.inbound.user_message")
|
|
1872
1904
|
)
|
|
1873
1905
|
continue;
|
|
1874
1906
|
const payload = effect.payload as SlackInboundEffectPayload;
|
|
@@ -1958,7 +1990,11 @@ export class SlackNotificationDaemon {
|
|
|
1958
1990
|
(payload.type === "reply" &&
|
|
1959
1991
|
routing.kind === "action" &&
|
|
1960
1992
|
effect.kind === "sdk.inbound.reply" &&
|
|
1961
|
-
payload.id === routing.actionId)
|
|
1993
|
+
payload.id === routing.actionId) ||
|
|
1994
|
+
(payload.type === "user_message" &&
|
|
1995
|
+
routing.kind === "message" &&
|
|
1996
|
+
effect.kind === "sdk.inbound.user_message" &&
|
|
1997
|
+
payload.sessionId === effect.sessionId));
|
|
1962
1998
|
let receipt: SlackInboundDispatchReceipt | undefined;
|
|
1963
1999
|
await this.store.transact(key, current => {
|
|
1964
2000
|
if (
|
|
@@ -1974,8 +2010,7 @@ export class SlackNotificationDaemon {
|
|
|
1974
2010
|
candidate =>
|
|
1975
2011
|
candidate.eventId === routing.eventId ||
|
|
1976
2012
|
candidate.interactionId === routing.interactionId ||
|
|
1977
|
-
candidate.retryKey === routing.retryKey
|
|
1978
|
-
(routing.eventContext !== undefined && candidate.eventContext === routing.eventContext),
|
|
2013
|
+
candidate.retryKey === routing.retryKey,
|
|
1979
2014
|
);
|
|
1980
2015
|
if (existing) {
|
|
1981
2016
|
if (this.#matchesInboundEffect(effect, existing)) receipt = existing;
|
|
@@ -1985,8 +2020,7 @@ export class SlackNotificationDaemon {
|
|
|
1985
2020
|
current.seenEventIds.includes(routing.eventId) ||
|
|
1986
2021
|
current.seenInteractionIds.includes(routing.interactionId) ||
|
|
1987
2022
|
current.seenRetryKeys.includes(routing.retryKey) ||
|
|
1988
|
-
(routing.
|
|
1989
|
-
(routing.kind === "action" && current.pendingActionId !== routing.actionId)
|
|
2023
|
+
(routing.kind === "action" && replyableActionId(current.pendingActionId) !== routing.actionId)
|
|
1990
2024
|
)
|
|
1991
2025
|
return current;
|
|
1992
2026
|
receipt = {
|
|
@@ -291,7 +291,7 @@ export const SDK_LIFECYCLE_ROUTER_PROTOCOL_VERSION = 1;
|
|
|
291
291
|
* killing a session's mirroring after one transient rejection and must be
|
|
292
292
|
* replaced across this upgrade.
|
|
293
293
|
*/
|
|
294
|
-
export const DAEMON_GENERATION =
|
|
294
|
+
export const DAEMON_GENERATION = 181;
|
|
295
295
|
|
|
296
296
|
/**
|
|
297
297
|
* Serving-compatibility boundary for daemon lifecycle requests. Epoch 7
|
package/src/sdk/cli/rows.ts
CHANGED
|
@@ -41,16 +41,20 @@ export interface SdkSessionRowV1 {
|
|
|
41
41
|
ambiguous?: boolean;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export interface
|
|
44
|
+
export interface SdkRingPositionV1 {
|
|
45
45
|
revision: number;
|
|
46
46
|
generation: number;
|
|
47
47
|
seq: number;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export interface SdkCheckpointRecordV1 extends SdkRingPositionV1 {
|
|
51
|
+
idle: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
50
54
|
export interface SdkRetentionGapV1 {
|
|
51
55
|
code: "retention_gap";
|
|
52
56
|
missing?: { from: number; to: number };
|
|
53
|
-
resync?:
|
|
57
|
+
resync?: SdkRingPositionV1;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
/** One tail item: a retained transcript entry or a live event-ring event. */
|
|
@@ -58,6 +62,8 @@ export interface SdkTailItemV1 {
|
|
|
58
62
|
/** "transcript" for retained transcript entries, otherwise the event-ring kind. */
|
|
59
63
|
kind: string;
|
|
60
64
|
id?: string;
|
|
65
|
+
/** Durable revision at emission; (revision,generation,seq) is unique per session. generation:seq alone is revision-local. */
|
|
66
|
+
revision?: number;
|
|
61
67
|
generation?: number;
|
|
62
68
|
seq?: number;
|
|
63
69
|
payload: unknown;
|
|
@@ -72,6 +78,43 @@ export interface SdkTailEnvelopeV1 {
|
|
|
72
78
|
items: SdkTailItemV1[];
|
|
73
79
|
}
|
|
74
80
|
|
|
81
|
+
export function tailItemKey(item: SdkTailItemV1): string {
|
|
82
|
+
// (generation, seq) restarts per revision; without the revision two turns'
|
|
83
|
+
// first frames share "1:1" and the second is silently dropped (#5200).
|
|
84
|
+
if (item.generation !== undefined && item.seq !== undefined) {
|
|
85
|
+
if (item.revision === undefined || !Number.isSafeInteger(item.revision) || item.revision < 0)
|
|
86
|
+
throw new Error("Cannot key a positioned tail item without its authoritative revision.");
|
|
87
|
+
return `${item.kind}\u0000${item.revision}\u0000${item.generation}\u0000${item.seq}`;
|
|
88
|
+
}
|
|
89
|
+
if (item.id !== undefined) return `${item.kind}\u0000${item.id}`;
|
|
90
|
+
return `${item.kind}\u0000${JSON.stringify(item.payload)}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Holds positioned live frames until the checkpoint can stamp their authoritative revision. */
|
|
94
|
+
export class TailRevisionBuffer {
|
|
95
|
+
#pending: SdkTailItemV1[] = [];
|
|
96
|
+
#resolved = false;
|
|
97
|
+
|
|
98
|
+
push(item: SdkTailItemV1): SdkTailItemV1[] {
|
|
99
|
+
if (item.revision !== undefined || item.generation === undefined || item.seq === undefined) return [item];
|
|
100
|
+
if (!this.#resolved) {
|
|
101
|
+
this.#pending.push(item);
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
throw new Error("A positioned live tail item arrived without an authoritative revision after checkpoint.");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
resolve(revision: number): SdkTailItemV1[] {
|
|
108
|
+
if (!Number.isSafeInteger(revision) || revision < 0)
|
|
109
|
+
throw new Error("Tail revision must be a non-negative integer.");
|
|
110
|
+
this.#resolved = true;
|
|
111
|
+
const pending = this.#pending;
|
|
112
|
+
this.#pending = [];
|
|
113
|
+
for (const item of pending) item.revision = revision;
|
|
114
|
+
return pending;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
75
118
|
const SECRET_FIELD = /(?:secret|token|password|credential|authorization|api[_-]?key)/i;
|
|
76
119
|
|
|
77
120
|
/** Recursively removes secret-shaped fields (defense in depth for DTO output). */
|
|
@@ -165,6 +208,7 @@ export function toCheckpointRecordV1(value: unknown): SdkCheckpointRecordV1 | un
|
|
|
165
208
|
const revision = value.revision;
|
|
166
209
|
const generation = value.generation;
|
|
167
210
|
const seq = value.seq;
|
|
211
|
+
const idle = value.idle;
|
|
168
212
|
if (
|
|
169
213
|
typeof revision !== "number" ||
|
|
170
214
|
!Number.isSafeInteger(revision) ||
|
|
@@ -174,10 +218,11 @@ export function toCheckpointRecordV1(value: unknown): SdkCheckpointRecordV1 | un
|
|
|
174
218
|
generation < 0 ||
|
|
175
219
|
typeof seq !== "number" ||
|
|
176
220
|
!Number.isSafeInteger(seq) ||
|
|
177
|
-
seq < 0
|
|
221
|
+
seq < 0 ||
|
|
222
|
+
typeof idle !== "boolean"
|
|
178
223
|
)
|
|
179
224
|
return undefined;
|
|
180
|
-
return { revision, generation, seq };
|
|
225
|
+
return { revision, generation, seq, idle };
|
|
181
226
|
}
|
|
182
227
|
|
|
183
228
|
export function toRetentionGapV1(value: unknown): SdkRetentionGapV1 | undefined {
|
|
@@ -189,15 +234,28 @@ export function toRetentionGapV1(value: unknown): SdkRetentionGapV1 | undefined
|
|
|
189
234
|
if (typeof from === "number" && Number.isSafeInteger(from) && typeof to === "number" && Number.isSafeInteger(to))
|
|
190
235
|
gap.missing = { from, to };
|
|
191
236
|
}
|
|
192
|
-
|
|
193
|
-
|
|
237
|
+
if (isRecord(value.resync)) {
|
|
238
|
+
const { revision, generation, seq } = value.resync;
|
|
239
|
+
if (
|
|
240
|
+
typeof revision === "number" &&
|
|
241
|
+
Number.isSafeInteger(revision) &&
|
|
242
|
+
revision >= 0 &&
|
|
243
|
+
typeof generation === "number" &&
|
|
244
|
+
Number.isSafeInteger(generation) &&
|
|
245
|
+
generation >= 0 &&
|
|
246
|
+
typeof seq === "number" &&
|
|
247
|
+
Number.isSafeInteger(seq) &&
|
|
248
|
+
seq >= 0
|
|
249
|
+
)
|
|
250
|
+
gap.resync = { revision, generation, seq };
|
|
251
|
+
}
|
|
194
252
|
return gap;
|
|
195
253
|
}
|
|
196
254
|
|
|
197
255
|
/** Projects a transcript entry or ring event into a tail item with dedupe keys. */
|
|
198
256
|
export function toTailItemV1(
|
|
199
257
|
value: unknown,
|
|
200
|
-
fallback: { kind: string; generation?: number; seq?: number },
|
|
258
|
+
fallback: { kind: string; revision?: number; generation?: number; seq?: number },
|
|
201
259
|
): SdkTailItemV1 {
|
|
202
260
|
if (!isRecord(value)) return { kind: fallback.kind, payload: stripSecretFields(value) };
|
|
203
261
|
const kind = optionalString(value.kind) ?? fallback.kind;
|
|
@@ -207,10 +265,20 @@ export function toTailItemV1(
|
|
|
207
265
|
const item: SdkTailItemV1 = {
|
|
208
266
|
kind,
|
|
209
267
|
...(payloadId !== undefined ? { id: payloadId } : id !== undefined ? { id } : {}),
|
|
268
|
+
...(typeof value.revision === "number" && Number.isSafeInteger(value.revision) && value.revision >= 0
|
|
269
|
+
? { revision: value.revision }
|
|
270
|
+
: {}),
|
|
210
271
|
...(typeof value.generation === "number" ? { generation: value.generation } : {}),
|
|
211
272
|
...(typeof value.seq === "number" ? { seq: value.seq } : {}),
|
|
212
273
|
payload: stripSecretFields(payload),
|
|
213
274
|
};
|
|
275
|
+
if (
|
|
276
|
+
fallback.revision !== undefined &&
|
|
277
|
+
Number.isSafeInteger(fallback.revision) &&
|
|
278
|
+
fallback.revision >= 0 &&
|
|
279
|
+
item.revision === undefined
|
|
280
|
+
)
|
|
281
|
+
item.revision = fallback.revision;
|
|
214
282
|
if (fallback.generation !== undefined && item.generation === undefined) item.generation = fallback.generation;
|
|
215
283
|
if (fallback.seq !== undefined && item.seq === undefined) item.seq = fallback.seq;
|
|
216
284
|
return item;
|