@gajae-code/coding-agent 0.16.1 → 0.16.4
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 +70 -0
- package/dist/types/cli/args.d.ts +2 -2
- package/dist/types/cli/root-flags.d.ts +4 -2
- package/dist/types/cli.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/discovery/claude-plugins.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/settings-selector.d.ts +6 -1
- package/dist/types/modes/components/status-line/command.d.ts +35 -0
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -0
- package/dist/types/modes/components/tool-status-header.d.ts +14 -0
- package/dist/types/modes/controllers/event-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/client.d.ts +9 -1
- package/dist/types/runtime-mcp/manager.d.ts +7 -1
- package/dist/types/runtime-mcp/pool.d.ts +22 -3
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/runtime-mcp/types.d.ts +22 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
- package/dist/types/sdk/cli/rows.d.ts +7 -0
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/session-runtime.d.ts +23 -0
- package/dist/types/sdk/session-api-key.d.ts +26 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/setup/paseo/announce.d.ts +111 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +2 -1
- package/dist/types/tools/fetch.d.ts +7 -0
- package/dist/types/tools/index.d.ts +8 -2
- package/dist/types/tools/path-utils.d.ts +2 -0
- package/dist/types/tools/search.d.ts +6 -0
- package/dist/types/utils/external-editor.d.ts +5 -0
- package/package.json +7 -7
- package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -0
- package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
- package/src/cli/args.ts +20 -9
- package/src/cli/root-flags.ts +5 -3
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/file-lock.ts +181 -17
- package/src/config/model-preset-registry.ts +5 -3
- package/src/config/model-profiles.ts +40 -0
- package/src/config/model-registry.ts +57 -11
- package/src/config/model-resolver.ts +48 -0
- package/src/config/settings-schema.ts +33 -1
- package/src/config/settings.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
- package/src/discovery/builtin.ts +13 -4
- package/src/discovery/claude-plugins.ts +12 -4
- package/src/discovery/helpers.ts +10 -2
- package/src/extensibility/gjc-plugins/compiler.ts +208 -58
- package/src/extensibility/gjc-plugins/installer.ts +13 -12
- package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
- package/src/extensibility/gjc-plugins/paths.ts +13 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
- package/src/extensibility/runtime-skill-discovery.ts +77 -151
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +44 -16
- package/src/gjc-runtime/repository-binding.ts +1 -1
- package/src/gjc-runtime/session-state-lock.ts +94 -6
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/main.ts +72 -34
- package/src/modes/DESIGN.md +54 -0
- package/src/modes/acp/acp-agent.ts +29 -14
- package/src/modes/components/assistant-message.ts +22 -9
- package/src/modes/components/model-selector.ts +166 -33
- package/src/modes/components/settings-selector.ts +1075 -235
- package/src/modes/components/status-line/command.ts +190 -0
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/segments.ts +32 -5
- package/src/modes/components/status-line/types.ts +5 -0
- package/src/modes/components/tool-status-header.ts +393 -21
- package/src/modes/controllers/command-controller.ts +2 -0
- package/src/modes/controllers/event-controller.ts +148 -5
- package/src/modes/controllers/selector-controller.ts +21 -0
- package/src/modes/interactive-mode.ts +47 -1
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/search.md +2 -0
- package/src/runtime-mcp/client.ts +60 -17
- package/src/runtime-mcp/manager.ts +171 -14
- package/src/runtime-mcp/pool.test.ts +209 -0
- package/src/runtime-mcp/pool.ts +224 -23
- package/src/runtime-mcp/transports/stdio.ts +158 -24
- package/src/runtime-mcp/types.ts +24 -0
- package/src/sdk/acp/adapter.ts +1 -1
- package/src/sdk/broker/broker.ts +5 -4
- package/src/sdk/bus/chat-daemon-control.ts +1 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +44 -1
- package/src/sdk/bus/slack-daemon.ts +92 -2
- package/src/sdk/bus/slack-live-provider.ts +12 -0
- package/src/sdk/bus/slack-provider.ts +5 -0
- package/src/sdk/cli/rows.ts +47 -2
- package/src/sdk/cli/session-cli.ts +47 -23
- package/src/sdk/host/events.ts +25 -2
- package/src/sdk/host/host.ts +4 -1
- package/src/sdk/host/session-runtime.test.ts +36 -25
- package/src/sdk/host/session-runtime.ts +279 -43
- package/src/sdk/protocol/operation-inventory.generated.json +66 -0
- package/src/sdk/router/session-router.ts +60 -37
- package/src/sdk/session-api-key.ts +94 -0
- package/src/sdk/session.ts +73 -30
- package/src/sdk/transport/serve-cli.ts +3 -3
- package/src/session/agent-session.ts +123 -38
- package/src/session/contribution-prep.ts +214 -21
- package/src/setup/paseo/announce.ts +450 -0
- package/src/setup/paseo/setup-deps.ts +15 -3
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +2 -1
- package/src/task/index.ts +17 -4
- package/src/tools/bash.ts +50 -21
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +29 -1
- package/src/tools/index.ts +8 -2
- package/src/tools/job.ts +8 -2
- package/src/tools/monitor.ts +8 -2
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/search.ts +116 -58
- package/src/tools/skill.ts +22 -2
- package/src/tools/subagent.ts +3 -1
- package/src/tools/tool-catalog.generated.ts +9 -2
- package/src/utils/external-editor.ts +16 -2
- package/src/web/insane/bridge.ts +113 -23
- package/src/web/search/providers/perplexity.ts +102 -41
|
@@ -136,7 +136,7 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
136
136
|
*/
|
|
137
137
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
138
138
|
discord: 73,
|
|
139
|
-
slack:
|
|
139
|
+
slack: 79,
|
|
140
140
|
};
|
|
141
141
|
|
|
142
142
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|
|
@@ -92,6 +92,36 @@ function isReservedIdentity(name: string | undefined): boolean {
|
|
|
92
92
|
return isLifecycleEvent(name) || isControlPlaneFrameType(name);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Chat notifications in lean mode are an answer surface, not a mirror of the
|
|
97
|
+
* session protocol. Lifecycle, identity, and activity frames remain available
|
|
98
|
+
* to verbose consumers, while finalized answers and action-needed events are
|
|
99
|
+
* delivered in lean mode.
|
|
100
|
+
*/
|
|
101
|
+
export function shouldPublishChatFrame(
|
|
102
|
+
frame: Record<string, unknown>,
|
|
103
|
+
verbosity: "lean" | "verbose" | undefined,
|
|
104
|
+
): boolean {
|
|
105
|
+
if (verbosity !== "lean") return frame.type !== "turn_stream" || frame.phase !== "live";
|
|
106
|
+
return frame.type === "turn_stream" && frame.phase === "finalized" && frame.finalAnswer === true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A final answer's message ref identifies its one rendered message even when
|
|
111
|
+
* the SDK transport delivers an unpositioned duplicate frame.
|
|
112
|
+
*/
|
|
113
|
+
export function publicationIdForFinalChatAnswer(sessionId: string, frame: Record<string, unknown>): string | undefined {
|
|
114
|
+
if (
|
|
115
|
+
frame.type !== "turn_stream" ||
|
|
116
|
+
frame.phase !== "finalized" ||
|
|
117
|
+
frame.finalAnswer !== true ||
|
|
118
|
+
typeof frame.messageRef !== "string" ||
|
|
119
|
+
frame.messageRef.length === 0
|
|
120
|
+
)
|
|
121
|
+
return undefined;
|
|
122
|
+
return `turn:${sessionId}:${frame.messageRef}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
95
125
|
/** One delivered frame reduced to a single event identity. */
|
|
96
126
|
type CorrelatedFrame = SessionRouterFrame;
|
|
97
127
|
|
|
@@ -471,7 +501,14 @@ export class ChatDaemonRuntime {
|
|
|
471
501
|
}
|
|
472
502
|
|
|
473
503
|
async #onAttachment(attachment: SessionAttachment): Promise<void> {
|
|
504
|
+
const revivedTransport = this.#attachments.get(attachment.sessionId) === attachment;
|
|
474
505
|
this.#attachments.set(attachment.sessionId, attachment);
|
|
506
|
+
if (revivedTransport) {
|
|
507
|
+
this.#presentation?.connectSession(attachment.sessionId, {
|
|
508
|
+
sendReply: route => attachment.send({ type: "reply", id: route.actionId, answer: route.answer }),
|
|
509
|
+
});
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
475
512
|
const discord = this.#discord;
|
|
476
513
|
const slack = this.#slack;
|
|
477
514
|
const barrier = (async () => {
|
|
@@ -564,8 +601,9 @@ export class ChatDaemonRuntime {
|
|
|
564
601
|
if (this.#attachments.get(attachment.sessionId) !== attachment) return;
|
|
565
602
|
await this.#attachmentBarriers.get(attachment.sessionId);
|
|
566
603
|
if (this.#attachments.get(attachment.sessionId) !== attachment) return;
|
|
567
|
-
const publicationId = correlated.publicationId;
|
|
568
604
|
const normalizedFrame = correlated.body;
|
|
605
|
+
const publicationId =
|
|
606
|
+
publicationIdForFinalChatAnswer(attachment.sessionId, normalizedFrame) ?? correlated.publicationId;
|
|
569
607
|
const bodyType = typeof normalizedFrame.type === "string" ? normalizedFrame.type : undefined;
|
|
570
608
|
if (isControlPlaneFrameType(correlated.name) || isControlPlaneFrameType(bodyType)) return;
|
|
571
609
|
if (normalizedFrame.type === "turn_stream" && normalizedFrame.phase === "live") return;
|
|
@@ -600,6 +638,11 @@ export class ChatDaemonRuntime {
|
|
|
600
638
|
return;
|
|
601
639
|
}
|
|
602
640
|
if (!notification) return;
|
|
641
|
+
if (
|
|
642
|
+
notification.type === "frame" &&
|
|
643
|
+
!shouldPublishChatFrame(normalizedFrame, this.input.config.presentation?.verbosity)
|
|
644
|
+
)
|
|
645
|
+
return;
|
|
603
646
|
const payload = this.#presentation?.fanout(notification)[0];
|
|
604
647
|
const body = payload?.body;
|
|
605
648
|
const content =
|
|
@@ -58,7 +58,9 @@ function slackPublicationClientMsgId(publicationId: string): string {
|
|
|
58
58
|
|
|
59
59
|
// Durable filesystem publication leases must outlast one event-loop and persistence turn.
|
|
60
60
|
const MIN_PUBLICATION_LEASE_MS = 100;
|
|
61
|
+
const INBOUND_REACTION_DEADLINE_MS = 5_000;
|
|
61
62
|
const RETIREMENT_DRAIN_MS = 5_000;
|
|
63
|
+
const INBOUND_REACTION = "eyes";
|
|
62
64
|
|
|
63
65
|
type SlackProviderWorkScope = {
|
|
64
66
|
sessionId: string;
|
|
@@ -70,6 +72,8 @@ type SlackProviderWork = {
|
|
|
70
72
|
scopeKey: string;
|
|
71
73
|
invalidate(): Promise<void>;
|
|
72
74
|
abort(): void;
|
|
75
|
+
/** Best-effort work is aborted before the graceful shutdown drain. */
|
|
76
|
+
abortOnStop?: boolean;
|
|
73
77
|
terminalize(): Promise<void>;
|
|
74
78
|
settled: Promise<void>;
|
|
75
79
|
};
|
|
@@ -362,6 +366,7 @@ export class SlackNotificationDaemon {
|
|
|
362
366
|
this.#startedGeneration = undefined;
|
|
363
367
|
if (this.#providerRunningGeneration === lifecycleGeneration) this.#providerRunningGeneration = undefined;
|
|
364
368
|
this.#clearLeaseRecoveryTimer();
|
|
369
|
+
for (const work of this.#workInvalidators) if (work.abortOnStop) work.abort();
|
|
365
370
|
if (stopProvider) this.#providerStopRequestedGenerations.add(lifecycleGeneration);
|
|
366
371
|
const providerStop = stopProvider ? Promise.resolve().then(() => this.options.provider.stop()) : undefined;
|
|
367
372
|
const providerStopResult = providerStop?.then(
|
|
@@ -449,6 +454,7 @@ export class SlackNotificationDaemon {
|
|
|
449
454
|
|
|
450
455
|
/** Accepted inbound effects are durably claimed before their Socket Mode ACK. */
|
|
451
456
|
async handleEnvelope(envelope: SlackSocketEnvelope): Promise<boolean> {
|
|
457
|
+
const lifecycleGeneration = this.#lifecycleGeneration;
|
|
452
458
|
if (!text(envelope.envelope_id)) return false;
|
|
453
459
|
const inbound = messageFromEnvelope(envelope.payload);
|
|
454
460
|
if (!inbound || inbound.teamId !== this.options.teamId || inbound.channelId !== this.options.channelId) {
|
|
@@ -479,6 +485,15 @@ export class SlackNotificationDaemon {
|
|
|
479
485
|
this.#inflightInbound.add(inflightKey);
|
|
480
486
|
try {
|
|
481
487
|
await this.options.provider.ack(envelope.envelope_id);
|
|
488
|
+
const messageTs = text(inbound.event.ts);
|
|
489
|
+
if (messageTs)
|
|
490
|
+
this.#scheduleInboundReaction(
|
|
491
|
+
claim.sessionId,
|
|
492
|
+
claim.endpoint,
|
|
493
|
+
inbound.channelId,
|
|
494
|
+
messageTs,
|
|
495
|
+
lifecycleGeneration,
|
|
496
|
+
);
|
|
482
497
|
return await this.#dispatchInbound(claim);
|
|
483
498
|
} finally {
|
|
484
499
|
this.#inflightInbound.delete(inflightKey);
|
|
@@ -491,6 +506,76 @@ export class SlackNotificationDaemon {
|
|
|
491
506
|
return work.finally(() => this.#activeWork.delete(work));
|
|
492
507
|
}
|
|
493
508
|
|
|
509
|
+
/** Track the best-effort acknowledgement in the owning attachment's provider-work scope. */
|
|
510
|
+
#scheduleInboundReaction(
|
|
511
|
+
sessionId: string,
|
|
512
|
+
endpoint: SessionAttachment,
|
|
513
|
+
channel: string,
|
|
514
|
+
timestamp: string,
|
|
515
|
+
lifecycleGeneration: number,
|
|
516
|
+
): void {
|
|
517
|
+
const scope: SlackProviderWorkScope = {
|
|
518
|
+
sessionId,
|
|
519
|
+
endpointGeneration: endpoint.generation,
|
|
520
|
+
...(endpoint.authorityId === undefined ? {} : { attachmentAuthorityId: endpoint.authorityId }),
|
|
521
|
+
};
|
|
522
|
+
const scopeKey = slackProviderWorkScopeKey(scope);
|
|
523
|
+
const workGeneration = this.#workGeneration;
|
|
524
|
+
const controller = new AbortController();
|
|
525
|
+
const settled = Promise.withResolvers<void>();
|
|
526
|
+
let abortKind: "deadline" | "lifecycle" | undefined;
|
|
527
|
+
const abort = (kind: "deadline" | "lifecycle"): void => {
|
|
528
|
+
abortKind ??= kind;
|
|
529
|
+
controller.abort();
|
|
530
|
+
};
|
|
531
|
+
const work: SlackProviderWork = {
|
|
532
|
+
scopeKey,
|
|
533
|
+
invalidate: async () => abort("lifecycle"),
|
|
534
|
+
abort: () => abort("lifecycle"),
|
|
535
|
+
abortOnStop: true,
|
|
536
|
+
terminalize: async () => abort("lifecycle"),
|
|
537
|
+
settled: settled.promise,
|
|
538
|
+
};
|
|
539
|
+
this.#workInvalidators.add(work);
|
|
540
|
+
const reaction = (async () => {
|
|
541
|
+
const deadline = setTimeout(() => abort("deadline"), INBOUND_REACTION_DEADLINE_MS);
|
|
542
|
+
try {
|
|
543
|
+
if (
|
|
544
|
+
workGeneration !== this.#workGeneration ||
|
|
545
|
+
lifecycleGeneration !== this.#lifecycleGeneration ||
|
|
546
|
+
this.#retiredProviderWorkScopes.has(scopeKey) ||
|
|
547
|
+
!endpoint.isCurrent()
|
|
548
|
+
) {
|
|
549
|
+
abort("lifecycle");
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
await this.options.provider.addReaction({
|
|
553
|
+
channel,
|
|
554
|
+
timestamp,
|
|
555
|
+
name: INBOUND_REACTION,
|
|
556
|
+
signal: controller.signal,
|
|
557
|
+
});
|
|
558
|
+
if (abortKind === "deadline")
|
|
559
|
+
logger.warn(
|
|
560
|
+
`Slack inbound acknowledgement reaction exceeded the ${INBOUND_REACTION_DEADLINE_MS}ms deadline and was aborted.`,
|
|
561
|
+
);
|
|
562
|
+
} catch {
|
|
563
|
+
if (abortKind !== "lifecycle")
|
|
564
|
+
logger.warn(
|
|
565
|
+
abortKind === "deadline"
|
|
566
|
+
? `Slack inbound acknowledgement reaction exceeded the ${INBOUND_REACTION_DEADLINE_MS}ms deadline and was aborted.`
|
|
567
|
+
: "Slack inbound acknowledgement reaction failed.",
|
|
568
|
+
);
|
|
569
|
+
} finally {
|
|
570
|
+
clearTimeout(deadline);
|
|
571
|
+
this.#workInvalidators.delete(work);
|
|
572
|
+
settled.resolve();
|
|
573
|
+
this.#tryReleaseRetiredProviderWorkScope(scopeKey);
|
|
574
|
+
}
|
|
575
|
+
})();
|
|
576
|
+
void this.#track(reaction);
|
|
577
|
+
}
|
|
578
|
+
|
|
494
579
|
async #invalidateActiveWork(): Promise<void> {
|
|
495
580
|
this.#workGeneration += 1;
|
|
496
581
|
await Promise.allSettled([...this.#workInvalidators].map(work => work.invalidate()));
|
|
@@ -1496,7 +1581,11 @@ export class SlackNotificationDaemon {
|
|
|
1496
1581
|
updatedAt: this.#now(),
|
|
1497
1582
|
});
|
|
1498
1583
|
});
|
|
1499
|
-
|
|
1584
|
+
if (!sessionId || !receipt) {
|
|
1585
|
+
await this.#journal.terminalize(effectId, { status: "rejected" });
|
|
1586
|
+
return undefined;
|
|
1587
|
+
}
|
|
1588
|
+
return { key, endpoint, sessionId, receipt };
|
|
1500
1589
|
}
|
|
1501
1590
|
|
|
1502
1591
|
async #dispatchInbound(claim: {
|
|
@@ -2020,7 +2109,8 @@ export class SlackNotificationDaemon {
|
|
|
2020
2109
|
current.seenEventIds.includes(routing.eventId) ||
|
|
2021
2110
|
current.seenInteractionIds.includes(routing.interactionId) ||
|
|
2022
2111
|
current.seenRetryKeys.includes(routing.retryKey) ||
|
|
2023
|
-
(routing.kind === "action" && replyableActionId(current.pendingActionId) !== routing.actionId)
|
|
2112
|
+
(routing.kind === "action" && replyableActionId(current.pendingActionId) !== routing.actionId) ||
|
|
2113
|
+
(routing.kind === "message" && replyableActionId(current.pendingActionId) !== undefined)
|
|
2024
2114
|
)
|
|
2025
2115
|
return current;
|
|
2026
2116
|
receipt = {
|
|
@@ -294,6 +294,18 @@ export class SlackLiveProvider implements SlackProviderClient, SlackDiagnosticPr
|
|
|
294
294
|
return message;
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
async addReaction(input: { channel: string; timestamp: string; name: string; signal?: AbortSignal }): Promise<void> {
|
|
298
|
+
await this.#api(
|
|
299
|
+
"reactions.add",
|
|
300
|
+
{
|
|
301
|
+
channel: input.channel,
|
|
302
|
+
timestamp: input.timestamp,
|
|
303
|
+
name: input.name,
|
|
304
|
+
},
|
|
305
|
+
input.signal,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
297
309
|
async findMessageByClientMsgId(input: {
|
|
298
310
|
channel: string;
|
|
299
311
|
clientMsgId: string;
|
|
@@ -57,6 +57,7 @@ export interface SlackProviderClient {
|
|
|
57
57
|
threadTs?: string;
|
|
58
58
|
signal?: AbortSignal;
|
|
59
59
|
}): Promise<SlackMessageSearchResult | null>;
|
|
60
|
+
addReaction?(input: { channel: string; timestamp: string; name: string; signal?: AbortSignal }): Promise<void>;
|
|
60
61
|
/**
|
|
61
62
|
* Prove that an operator-supplied timestamp addresses a real message in the
|
|
62
63
|
* requested channel. Adoption of an existing root is refused when a client
|
|
@@ -108,6 +109,10 @@ export class SlackProvider {
|
|
|
108
109
|
return (await this.client.findMessageByClientMsgId?.(input)) ?? null;
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
async addReaction(input: { channel: string; timestamp: string; name: string; signal?: AbortSignal }): Promise<void> {
|
|
113
|
+
await this.client.addReaction?.(input);
|
|
114
|
+
}
|
|
115
|
+
|
|
111
116
|
/** Fail-closed root verification: a client without this capability can never confirm a root. */
|
|
112
117
|
async findMessageByTimestamp(input: { channel: string; ts: string }): Promise<SlackMessageSearchResult | null> {
|
|
113
118
|
return (await this.client.findMessageByTimestamp?.(input)) ?? null;
|
package/src/sdk/cli/rows.ts
CHANGED
|
@@ -78,6 +78,43 @@ export interface SdkTailEnvelopeV1 {
|
|
|
78
78
|
items: SdkTailItemV1[];
|
|
79
79
|
}
|
|
80
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
|
+
|
|
81
118
|
const SECRET_FIELD = /(?:secret|token|password|credential|authorization|api[_-]?key)/i;
|
|
82
119
|
|
|
83
120
|
/** Recursively removes secret-shaped fields (defense in depth for DTO output). */
|
|
@@ -228,12 +265,20 @@ export function toTailItemV1(
|
|
|
228
265
|
const item: SdkTailItemV1 = {
|
|
229
266
|
kind,
|
|
230
267
|
...(payloadId !== undefined ? { id: payloadId } : id !== undefined ? { id } : {}),
|
|
231
|
-
...(typeof value.revision === "number"
|
|
268
|
+
...(typeof value.revision === "number" && Number.isSafeInteger(value.revision) && value.revision >= 0
|
|
269
|
+
? { revision: value.revision }
|
|
270
|
+
: {}),
|
|
232
271
|
...(typeof value.generation === "number" ? { generation: value.generation } : {}),
|
|
233
272
|
...(typeof value.seq === "number" ? { seq: value.seq } : {}),
|
|
234
273
|
payload: stripSecretFields(payload),
|
|
235
274
|
};
|
|
236
|
-
if (
|
|
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;
|
|
237
282
|
if (fallback.generation !== undefined && item.generation === undefined) item.generation = fallback.generation;
|
|
238
283
|
if (fallback.seq !== undefined && item.seq === undefined) item.seq = fallback.seq;
|
|
239
284
|
return item;
|
|
@@ -38,10 +38,13 @@ import { SESSION_REQUEST_TIMEOUT_MS } from "../session-reconnect";
|
|
|
38
38
|
import {
|
|
39
39
|
type SdkCheckpointRecordV1,
|
|
40
40
|
type SdkRetentionGapV1,
|
|
41
|
+
type SdkRingPositionV1,
|
|
41
42
|
type SdkSessionRowV1,
|
|
42
43
|
type SdkTailItemV1,
|
|
43
44
|
SESSION_ROWS_VERSION,
|
|
44
45
|
stripSecretFields,
|
|
46
|
+
TailRevisionBuffer,
|
|
47
|
+
tailItemKey,
|
|
45
48
|
toCheckpointRecordV1,
|
|
46
49
|
toRetentionGapV1,
|
|
47
50
|
toSessionRowV1,
|
|
@@ -331,13 +334,14 @@ function throwResponseFailure(response: unknown): void {
|
|
|
331
334
|
async function paginatedSessionList(
|
|
332
335
|
router: SessionRouter,
|
|
333
336
|
input: JsonRecord = {},
|
|
334
|
-
requestKey = `${SDK_SESSION_CLI_LIFECYCLE_ACTOR.namespace}:session.list`,
|
|
337
|
+
requestKey = `${SDK_SESSION_CLI_LIFECYCLE_ACTOR.namespace}:session.list:${randomBytes(12).toString("hex")}`,
|
|
335
338
|
): Promise<unknown> {
|
|
336
339
|
try {
|
|
337
340
|
const pages = await traverseSessionList(
|
|
338
341
|
input,
|
|
339
342
|
async pageInput => {
|
|
340
|
-
const
|
|
343
|
+
const cursor = typeof pageInput.cursor === "string" ? pageInput.cursor : "initial";
|
|
344
|
+
const response = object(await router.listBrokerSessions(pageInput, `${requestKey}:${cursor}`));
|
|
341
345
|
if (response?.ok === false) {
|
|
342
346
|
const failure = object(response.error);
|
|
343
347
|
throw new SdkClientError(
|
|
@@ -372,10 +376,10 @@ type SessionRows = {
|
|
|
372
376
|
sessions: SdkSessionRowV1[];
|
|
373
377
|
};
|
|
374
378
|
|
|
375
|
-
async function sessionRows(agentDir: string): Promise<SessionRows> {
|
|
379
|
+
async function sessionRows(agentDir: string, input: JsonRecord = {}): Promise<SessionRows> {
|
|
376
380
|
await ensureBroker({ agentDir });
|
|
377
381
|
return await withRouter(agentDir, async router => {
|
|
378
|
-
const response = await paginatedSessionList(router);
|
|
382
|
+
const response = await paginatedSessionList(router, input);
|
|
379
383
|
const result = resultObject(response) ?? {};
|
|
380
384
|
let sessions: SdkSessionRowV1[];
|
|
381
385
|
try {
|
|
@@ -686,7 +690,7 @@ async function runList(agentDir: string, args: SdkSessionCliArgs): Promise<unkno
|
|
|
686
690
|
}
|
|
687
691
|
|
|
688
692
|
async function runInspect(agentDir: string, sessionId: string): Promise<unknown> {
|
|
689
|
-
const listing = await sessionRows(agentDir);
|
|
693
|
+
const listing = await sessionRows(agentDir, { resolveSessionId: sessionId });
|
|
690
694
|
const session = listing.sessions.find(candidate => candidate.sessionId === sessionId);
|
|
691
695
|
if (!session)
|
|
692
696
|
throw new SdkSessionCliError("session_unavailable", `Session ${sessionId} is not indexed by the broker.`, 1);
|
|
@@ -985,13 +989,6 @@ function extractTranscriptPage(response: unknown): { items: unknown[]; complete:
|
|
|
985
989
|
};
|
|
986
990
|
}
|
|
987
991
|
|
|
988
|
-
function tailItemKey(item: SdkTailItemV1): string {
|
|
989
|
-
if (item.generation !== undefined && item.seq !== undefined)
|
|
990
|
-
return `${item.kind}\u0000${item.generation}\u0000${item.seq}`;
|
|
991
|
-
if (item.id !== undefined) return `${item.kind}\u0000${item.id}`;
|
|
992
|
-
return `${item.kind}\u0000${JSON.stringify(item.payload)}`;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
992
|
function mergeTailItems(
|
|
996
993
|
target: SdkTailItemV1[],
|
|
997
994
|
seen: Set<string>,
|
|
@@ -1006,8 +1003,9 @@ function mergeTailItems(
|
|
|
1006
1003
|
}
|
|
1007
1004
|
}
|
|
1008
1005
|
|
|
1009
|
-
/** Canonical ring position of an event item, when the host stated one. */
|
|
1010
|
-
type TailSeqKey =
|
|
1006
|
+
/** Canonical session-wide ring position of an event item, when the host stated one. */
|
|
1007
|
+
type TailSeqKey = SdkRingPositionV1;
|
|
1008
|
+
type TailLocalPosition = Pick<SdkRingPositionV1, "generation" | "seq">;
|
|
1011
1009
|
|
|
1012
1010
|
function isPositionCoordinate(value: unknown): value is number {
|
|
1013
1011
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
@@ -1057,7 +1055,7 @@ function classifyRawEventPosition(event: unknown): "unpositioned" | "malformed"
|
|
|
1057
1055
|
* it is never silently downgraded to unpositioned. Claiming neither coordinate
|
|
1058
1056
|
* is a genuine unpositioned event.
|
|
1059
1057
|
*/
|
|
1060
|
-
function classifyTailPosition(item: SdkTailItemV1):
|
|
1058
|
+
function classifyTailPosition(item: SdkTailItemV1): TailLocalPosition | "unpositioned" | "malformed" {
|
|
1061
1059
|
if (item.generation === undefined && item.seq === undefined) return "unpositioned";
|
|
1062
1060
|
if (!isPositionCoordinate(item.generation) || !isPositionCoordinate(item.seq)) return "malformed";
|
|
1063
1061
|
return { generation: item.generation, seq: item.seq };
|
|
@@ -1065,10 +1063,13 @@ function classifyTailPosition(item: SdkTailItemV1): TailSeqKey | "unpositioned"
|
|
|
1065
1063
|
|
|
1066
1064
|
function tailSeqKey(item: SdkTailItemV1): TailSeqKey | undefined {
|
|
1067
1065
|
const position = classifyTailPosition(item);
|
|
1068
|
-
return typeof position === "string"
|
|
1066
|
+
return typeof position === "string" || !isPositionCoordinate(item.revision)
|
|
1067
|
+
? undefined
|
|
1068
|
+
: { revision: item.revision, ...position };
|
|
1069
1069
|
}
|
|
1070
1070
|
|
|
1071
1071
|
function compareTailSeqKeys(left: TailSeqKey, right: TailSeqKey): number {
|
|
1072
|
+
if (left.revision !== right.revision) return left.revision - right.revision;
|
|
1072
1073
|
return left.generation !== right.generation ? left.generation - right.generation : left.seq - right.seq;
|
|
1073
1074
|
}
|
|
1074
1075
|
|
|
@@ -1112,10 +1113,12 @@ function mergeEventTailItems(
|
|
|
1112
1113
|
target.unpositioned.push(item);
|
|
1113
1114
|
continue;
|
|
1114
1115
|
}
|
|
1116
|
+
const orderedPosition = tailSeqKey(item);
|
|
1117
|
+
if (orderedPosition === undefined) throw new Error("A positioned tail item has no authoritative revision.");
|
|
1115
1118
|
let index = target.positioned.length;
|
|
1116
1119
|
for (; index > 0; index--) {
|
|
1117
1120
|
const previous = tailSeqKey(target.positioned[index - 1]!);
|
|
1118
|
-
if (previous === undefined || compareTailSeqKeys(previous,
|
|
1121
|
+
if (previous === undefined || compareTailSeqKeys(previous, orderedPosition) <= 0) break;
|
|
1119
1122
|
}
|
|
1120
1123
|
target.positioned.splice(index, 0, item);
|
|
1121
1124
|
}
|
|
@@ -1158,11 +1161,13 @@ function tailItemFromRouterFrame(frame: SessionRouterFrame): SdkTailItemV1 | und
|
|
|
1158
1161
|
return toTailItemV1(
|
|
1159
1162
|
{
|
|
1160
1163
|
kind: frame.name,
|
|
1164
|
+
...(frame.revision === undefined ? {} : { revision: frame.revision }),
|
|
1161
1165
|
...(seq === undefined ? {} : { generation: frame.generation, seq }),
|
|
1162
1166
|
payload: frame.body,
|
|
1163
1167
|
},
|
|
1164
1168
|
{
|
|
1165
1169
|
kind: frame.name,
|
|
1170
|
+
...(frame.revision === undefined ? {} : { revision: frame.revision }),
|
|
1166
1171
|
...(seq === undefined ? {} : { generation: frame.generation, seq }),
|
|
1167
1172
|
},
|
|
1168
1173
|
);
|
|
@@ -1406,6 +1411,7 @@ async function runLiveTail(
|
|
|
1406
1411
|
// evidence.
|
|
1407
1412
|
const seenTranscript = new Set<string>();
|
|
1408
1413
|
const seenEvents = new Set<string>();
|
|
1414
|
+
const liveRevisionBuffer = new TailRevisionBuffer();
|
|
1409
1415
|
let checkpoint: SdkCheckpointRecordV1 | undefined;
|
|
1410
1416
|
let gap: SdkRetentionGapV1 | undefined;
|
|
1411
1417
|
let liveReason: TailExitReason | undefined;
|
|
@@ -1445,7 +1451,7 @@ async function runLiveTail(
|
|
|
1445
1451
|
// close at a contested position win instead of failing closed, and
|
|
1446
1452
|
// checking it after the monotonic guard would silently skip a
|
|
1447
1453
|
// conflicting kind at an already-decided position.
|
|
1448
|
-
const positionKey = `${position.generation}\u0000${position.seq}`;
|
|
1454
|
+
const positionKey = `${position.revision}\u0000${position.generation}\u0000${position.seq}`;
|
|
1449
1455
|
const claimed = claimedLifecycleKinds.get(positionKey);
|
|
1450
1456
|
if (claimed !== undefined && claimed !== item.kind) {
|
|
1451
1457
|
malformed ??= new SdkSessionCliError(
|
|
@@ -1487,9 +1493,17 @@ async function runLiveTail(
|
|
|
1487
1493
|
if (attachment.sessionId !== sessionId) return;
|
|
1488
1494
|
const item = tailItemFromRouterFrame(frame);
|
|
1489
1495
|
if (!item) return;
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1496
|
+
try {
|
|
1497
|
+
applyLifecycle(mergeEventTailItems(eventItems, seenEvents, liveRevisionBuffer.push(item), include));
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
malformed ??= new SdkSessionCliError(
|
|
1500
|
+
"protocol_error",
|
|
1501
|
+
error instanceof Error ? error.message : "A live tail item had no authoritative revision.",
|
|
1502
|
+
1,
|
|
1503
|
+
{ sessionId, kind: item.kind, generation: item.generation, seq: item.seq },
|
|
1504
|
+
);
|
|
1505
|
+
rejectLive?.(malformed);
|
|
1506
|
+
}
|
|
1493
1507
|
};
|
|
1494
1508
|
|
|
1495
1509
|
return await withRouter(
|
|
@@ -1511,6 +1525,10 @@ async function runLiveTail(
|
|
|
1511
1525
|
const extraction = extractCheckpoint(checkpointResponse);
|
|
1512
1526
|
checkpoint = extraction.record;
|
|
1513
1527
|
gap = extraction.gap;
|
|
1528
|
+
if (checkpoint !== undefined)
|
|
1529
|
+
applyLifecycle(
|
|
1530
|
+
mergeEventTailItems(eventItems, seenEvents, liveRevisionBuffer.resolve(checkpoint.revision), include),
|
|
1531
|
+
);
|
|
1514
1532
|
if (gap !== undefined && args.strict === true)
|
|
1515
1533
|
throw new SdkSessionCliError(
|
|
1516
1534
|
"retention_gap",
|
|
@@ -1595,7 +1613,11 @@ async function runLiveTail(
|
|
|
1595
1613
|
applyLifecycle(mergeEventTailItems(eventItems, seenEvents, replayItems, include));
|
|
1596
1614
|
if (malformed !== undefined) throw malformed;
|
|
1597
1615
|
if (checkpoint !== undefined) {
|
|
1598
|
-
const checkpointKey = {
|
|
1616
|
+
const checkpointKey = {
|
|
1617
|
+
revision: checkpoint.revision,
|
|
1618
|
+
generation: checkpoint.generation,
|
|
1619
|
+
seq: checkpoint.seq,
|
|
1620
|
+
};
|
|
1599
1621
|
if (turnStateKey === undefined || compareTailSeqKeys(checkpointKey, turnStateKey) >= 0) {
|
|
1600
1622
|
turnStateKey = checkpointKey;
|
|
1601
1623
|
turnIdle = checkpoint.idle;
|
|
@@ -1650,7 +1672,9 @@ export async function runTail(
|
|
|
1650
1672
|
sessionId: string,
|
|
1651
1673
|
args: SdkSessionCliArgs,
|
|
1652
1674
|
): Promise<unknown> {
|
|
1653
|
-
const row = (await sessionRows(agentDir)).sessions.find(
|
|
1675
|
+
const row = (await sessionRows(agentDir, { resolveSessionId: sessionId })).sessions.find(
|
|
1676
|
+
candidate => candidate.sessionId === sessionId,
|
|
1677
|
+
);
|
|
1654
1678
|
if (!row)
|
|
1655
1679
|
throw new SdkSessionCliError("session_unavailable", `Session ${sessionId} is not indexed by the broker.`, 1);
|
|
1656
1680
|
if (row.deleted)
|
package/src/sdk/host/events.ts
CHANGED
|
@@ -32,11 +32,20 @@ export class SessionEventStream {
|
|
|
32
32
|
#frames: EventFrame[] = [];
|
|
33
33
|
readonly #ringSize: number;
|
|
34
34
|
readonly #resyncQueries: string[];
|
|
35
|
+
readonly #revisionProvider: (() => number | undefined) | undefined;
|
|
35
36
|
|
|
36
|
-
constructor(
|
|
37
|
+
constructor(
|
|
38
|
+
options: {
|
|
39
|
+
generation?: number;
|
|
40
|
+
ringSize?: number;
|
|
41
|
+
resyncQueryIds?: string[];
|
|
42
|
+
revisionProvider?: () => number | undefined;
|
|
43
|
+
} = {},
|
|
44
|
+
) {
|
|
37
45
|
this.#generation = options.generation ?? 0;
|
|
38
46
|
this.#ringSize = options.ringSize ?? 256;
|
|
39
47
|
this.#resyncQueries = options.resyncQueryIds ?? ["Q01", "Q02", "Q03"];
|
|
48
|
+
this.#revisionProvider = options.revisionProvider;
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
get generation(): number {
|
|
@@ -54,7 +63,21 @@ export class SessionEventStream {
|
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
emit(frame: SdkFrame): EventFrame {
|
|
57
|
-
const
|
|
66
|
+
const suppliedRevision = frame.revision;
|
|
67
|
+
const providedRevision = this.#revisionProvider?.();
|
|
68
|
+
const revision =
|
|
69
|
+
typeof suppliedRevision === "number" && Number.isSafeInteger(suppliedRevision) && suppliedRevision >= 0
|
|
70
|
+
? suppliedRevision
|
|
71
|
+
: typeof providedRevision === "number" && Number.isSafeInteger(providedRevision) && providedRevision >= 0
|
|
72
|
+
? providedRevision
|
|
73
|
+
: undefined;
|
|
74
|
+
const event: EventFrame = {
|
|
75
|
+
...frame,
|
|
76
|
+
type: "event",
|
|
77
|
+
...(revision === undefined ? {} : { revision }),
|
|
78
|
+
generation: this.#generation,
|
|
79
|
+
seq: ++this.#seq,
|
|
80
|
+
};
|
|
58
81
|
this.#frames.push(event);
|
|
59
82
|
if (this.#frames.length > this.#ringSize) this.#frames.shift();
|
|
60
83
|
return event;
|
package/src/sdk/host/host.ts
CHANGED
|
@@ -47,6 +47,8 @@ export type SessionActivationGate = (input: { sessionId: string; generation: num
|
|
|
47
47
|
export interface SessionSdkHostOptions extends HostEndpointAdapters {
|
|
48
48
|
control?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
|
|
49
49
|
query?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
|
|
50
|
+
/** Supplies the durable transcript revision stamped on ordinary emitted events. */
|
|
51
|
+
eventRevision?: () => number | undefined;
|
|
50
52
|
/** Test/lifecycle seam invoked synchronously before fire-and-forget dispatch. */
|
|
51
53
|
onFrameAdmitted?: (connectionId: string, frame: SdkFrame) => void;
|
|
52
54
|
/** Synchronously rejects external input while a runtime is being replaced. */
|
|
@@ -199,7 +201,7 @@ function has(frame: SdkFrame, field: string): boolean {
|
|
|
199
201
|
|
|
200
202
|
/** Adapter-based session host; bus wiring owns NotificationServer creation and transport framing. */
|
|
201
203
|
export class SessionSdkHost {
|
|
202
|
-
readonly events
|
|
204
|
+
readonly events: SessionEventStream;
|
|
203
205
|
readonly reverse: ReverseLeaseRuntime;
|
|
204
206
|
readonly #options: SessionSdkHostOptions;
|
|
205
207
|
#started = false;
|
|
@@ -214,6 +216,7 @@ export class SessionSdkHost {
|
|
|
214
216
|
|
|
215
217
|
constructor(options: SessionSdkHostOptions) {
|
|
216
218
|
this.#options = options;
|
|
219
|
+
this.events = new SessionEventStream({ revisionProvider: options.eventRevision });
|
|
217
220
|
this.reverse = new ReverseLeaseRuntime({
|
|
218
221
|
sendFrame: options.sendFrame,
|
|
219
222
|
installDefinitions: options.installProviderDefinitions,
|