@stratusagent/channel-slack 0.12.6 → 0.13.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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { constants } from 'node:fs';
3
- import { lstat, mkdir, open, readdir, readFile, realpath } from 'node:fs/promises';
3
+ import { lstat, mkdir, open, readdir, readFile, realpath, rename, rm, writeFile } from 'node:fs/promises';
4
4
  import { createRequire } from 'node:module';
5
5
  import path from 'node:path';
6
6
  import { filePathsOf, isLatestTurn, latestTurnReply, MAX_IDEMPOTENCY_KEY_LENGTH, turnFailureFor, turnFilesFor, turnReplyFor, CONVERSATION_METADATA_KEY, SENDER_TRUST_METADATA_KEY, sessionWriteTrust, imageDimensions, isImageAttachmentMediaType, IMAGE_ATTACHMENT_MAX_BYTES, IMAGE_ATTACHMENT_MAX_DIMENSION, IMAGE_ATTACHMENTS_MAX_TOTAL_BYTES, escapeControlCharacters, RunAbortedError, leastTrusted, sessionTrustOf, SESSION_TRUST_METADATA_KEY, } from '@stratusagent/core';
@@ -55,6 +55,17 @@ const THREAD_SENDER_CAPACITY = 64;
55
55
  * socket stays behind for all of them.
56
56
  */
57
57
  const THREAD_HANDOVER_DEPTH = 32;
58
+ /**
59
+ * How far past this host's clock at `start()` claims count as seen live
60
+ * (card 562). Slack stamps messages by its own clock, so a host clock
61
+ * behind Slack's would date a message sent just before the restart as
62
+ * after it, and a read stopping there would look complete. Reads for
63
+ * messages in this first stretch are treated as stopping short, which
64
+ * costs a thread read or two, never a wrong placement.
65
+ */
66
+ const LIVE_FROM_MARGIN_MS = 60_000;
67
+ /** `liveFrom` while `start()` runs: later than any message. */
68
+ const NOT_LIVE_YET = '9999999999.999999';
58
69
  /**
59
70
  * How many agent mentions one thread admits in a row before a person has
60
71
  * to speak there again (card 513): enough for a question, an answer and a
@@ -114,6 +125,33 @@ const EARLIER_CHANNEL_LIMIT = 20;
114
125
  const EARLIER_CHANNEL_READ = 100;
115
126
  /** The most pages read to find them: 1,000 messages. */
116
127
  const EARLIER_CHANNEL_PAGES = 10;
128
+ /** What a person who wrote during a restart is told, when it was kept. */
129
+ export const RESTARTING_REPLY_TEXT = "I'm restarting right now. I'll answer this as soon as I'm back.";
130
+ /** The same, for a daemon that is stopping rather than restarting. */
131
+ export const STOPPING_REPLY_TEXT = "I'm shutting down right now. I'll answer this when I'm started again.";
132
+ /**
133
+ * How old a kept message may be and still be answered at start: a daemon
134
+ * stopped for a week does not wake up to answer last week's questions.
135
+ */
136
+ const PENDING_INTAKE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
137
+ /**
138
+ * How long the start-up replay waits on Slack to read one kept message
139
+ * back. Past it, that message (and any kept after it in its conversation)
140
+ * is left for the next start rather than answered late; see `replayHolds`.
141
+ */
142
+ const REPLAY_READ_MAX_MS = 10_000;
143
+ /** The most messages kept at once; past it, a new one is refused, not kept. */
144
+ const PENDING_INTAKE_MAX = 200;
145
+ const isPendingIntake = (value) => {
146
+ if (typeof value !== 'object' || value === null) {
147
+ return false;
148
+ }
149
+ const entry = value;
150
+ const optionalString = (field) => field === undefined || typeof field === 'string';
151
+ return typeof entry.agentId === 'string' && typeof entry.team === 'string' && typeof entry.channel === 'string'
152
+ && typeof entry.ts === 'string' && typeof entry.user === 'string' && typeof entry.keptAt === 'number'
153
+ && optionalString(entry.threadTs) && optionalString(entry.channelType);
154
+ };
117
155
  // Lazy (and CJS-interoperable) so tests with injected fakes never load the
118
156
  // real SDK stack.
119
157
  const requireModule = createRequire(import.meta.url);
@@ -1046,19 +1084,19 @@ class ReplyRenderer {
1046
1084
  // that would take the next turn's status down with it.
1047
1085
  await this.statusChain;
1048
1086
  }
1049
- return this.outcome(published, landed || this.edited || this.uploaded || stranded);
1087
+ return this.outcome(published, landed || this.edited || this.uploaded || stranded, !published);
1050
1088
  }
1051
1089
  placeText(ts) {
1052
1090
  if (ts !== undefined && (this.textAt === undefined || ts > this.textAt)) {
1053
1091
  this.textAt = ts;
1054
1092
  }
1055
1093
  }
1056
- outcome(published, spoke) {
1094
+ outcome(published, spoke, refused = false) {
1057
1095
  const spokeAt = [this.textAt, this.uploadedAt]
1058
1096
  .filter((ts) => ts !== undefined)
1059
1097
  .sort()
1060
1098
  .at(-1);
1061
- return { published, spoke, ...(spoke && spokeAt !== undefined ? { spokeAt } : {}) };
1099
+ return { published, spoke, ...(spoke && spokeAt !== undefined ? { spokeAt } : {}), ...(refused ? { refused: true } : {}) };
1062
1100
  }
1063
1101
  /**
1064
1102
  * Take back the placeholder a turn nobody asked for opened and then had
@@ -2188,6 +2226,13 @@ const compareSlackTs = (left, right) => {
2188
2226
  }
2189
2227
  return Math.sign(Number(leftMicros.padEnd(6, '0').slice(0, 6)) - Number(rightMicros.padEnd(6, '0').slice(0, 6)));
2190
2228
  };
2229
+ /** What a Slack stop aborts a turn with, followed by the stopper's user id. */
2230
+ const SLACK_STOP_REASON = 'Stopped from Slack by ';
2231
+ /**
2232
+ * Who stopped a turn, read back from the error it failed with: the gateway
2233
+ * fails an aborted turn with the abort's reason verbatim.
2234
+ */
2235
+ const slackStopper = (error) => error?.startsWith(SLACK_STOP_REASON) ? error.slice(SLACK_STOP_REASON.length) || undefined : undefined;
2191
2236
  export const createSlackChannelAdapter = (options) => {
2192
2237
  const log = options.log ?? (() => { });
2193
2238
  const warn = options.warn ?? (() => { });
@@ -2283,6 +2328,26 @@ export const createSlackChannelAdapter = (options) => {
2283
2328
  * corrects it.
2284
2329
  */
2285
2330
  const threadAddressee = new Map();
2331
+ /**
2332
+ * Per thread whose record has been cut in the middle (`trimHandovers`),
2333
+ * the first claim kept after the cut. Who held the thread between its
2334
+ * first claim and that one is no longer known here (card 559): a message
2335
+ * from then is left to the sessions and a thread read, rather than
2336
+ * placed by the first claim, or the latest take, that outlived the cut.
2337
+ * Goes with the thread's record.
2338
+ */
2339
+ const threadTrimmedUntil = new Map();
2340
+ /**
2341
+ * Per thread the record was warmed from a thread read (#369), the last
2342
+ * message a read covered. A read stops at the message it places, so one
2343
+ * for a message from before this process started (delivered late, or a
2344
+ * replay after a restart) leaves out every take between that message and
2345
+ * the start (card 562). `liveFrom` is when every socket was up, plus a
2346
+ * margin for clock skew: claims since were seen live. Goes with the
2347
+ * thread's record.
2348
+ */
2349
+ const threadReadUntil = new Map();
2350
+ let liveFrom;
2286
2351
  /**
2287
2352
  * Threads whose owner this process can name (card 508): `true` for one
2288
2353
  * whose first message it saw, so the first claim in `threadAddressee` is
@@ -2422,6 +2487,332 @@ export const createSlackChannelAdapter = (options) => {
2422
2487
  inflight.add(settled);
2423
2488
  void settled.finally(() => inflight.delete(settled));
2424
2489
  };
2490
+ /**
2491
+ * The kept messages (see `pendingIntakePath`), read and written one change
2492
+ * at a time so two refusals landing together cannot drop each other.
2493
+ */
2494
+ const pendingIntakePath = options.pendingIntakePath;
2495
+ let pendingChain = Promise.resolve();
2496
+ const readPending = async (file) => {
2497
+ let raw;
2498
+ try {
2499
+ raw = await readFile(file, 'utf8');
2500
+ }
2501
+ catch (error) {
2502
+ if (error.code === 'ENOENT') {
2503
+ return [];
2504
+ }
2505
+ throw error;
2506
+ }
2507
+ try {
2508
+ const parsed = JSON.parse(raw);
2509
+ if (Array.isArray(parsed)) {
2510
+ return parsed.filter(isPendingIntake);
2511
+ }
2512
+ }
2513
+ catch {
2514
+ // Named below, and replaced by the next write.
2515
+ }
2516
+ warn(`slack: ${file} is not a list of kept messages; ignoring it`);
2517
+ return [];
2518
+ };
2519
+ const changePending = (change) => {
2520
+ const file = pendingIntakePath;
2521
+ if (file === undefined) {
2522
+ return Promise.reject(new Error('no pendingIntakePath'));
2523
+ }
2524
+ const run = pendingChain.then(async () => {
2525
+ const before = await readPending(file);
2526
+ const { entries, result } = change(before);
2527
+ if (entries.length === 0) {
2528
+ await rm(file, { force: true });
2529
+ }
2530
+ else {
2531
+ // Whole or not at all: a crash mid-write must not leave half a list
2532
+ // that the next start reads as no list.
2533
+ const temporary = `${file}.${process.pid}.tmp`;
2534
+ await writeFile(temporary, `${JSON.stringify(entries, null, 2)}\n`, { mode: 0o600 });
2535
+ await rename(temporary, file);
2536
+ }
2537
+ return result;
2538
+ });
2539
+ pendingChain = run.catch(() => undefined);
2540
+ return run;
2541
+ };
2542
+ const pendingKey = (entry) => `${entry.agentId}:${entry.team}:${entry.channel}:${entry.ts}`;
2543
+ /**
2544
+ * Kept messages being replayed, by `pendingKey`. A replayed message comes
2545
+ * off the list only once its answer is confirmed (`answered`): an answer
2546
+ * or failure Slack took, a turn that chose to say nothing, or the
2547
+ * start-up recovery's own report of it reaching the thread. Every other
2548
+ * way a handler can end leaves it kept, so a path that forgets to say
2549
+ * how it went keeps the message rather than losing it. A message kept
2550
+ * again by a restart during the replay (`rekept`) stays kept too. The
2551
+ * next start hands it over again, and the gateway repeats the finished
2552
+ * turn, so the answer is posted from the session rather than run again.
2553
+ * `dispatched` and `refused` only choose the warning.
2554
+ */
2555
+ const replaying = new Map();
2556
+ /** Notes a reply outcome against the kept message it answers, if that one is being replayed. */
2557
+ const noteReplayOutcome = (connection, channel, ts, outcome) => {
2558
+ const replay = replaying.get(pendingKey({ agentId: connection.config.agentId, team: connection.teamId, channel, ts }));
2559
+ if (replay !== undefined) {
2560
+ if (outcome.refused === true) {
2561
+ replay.refused = true;
2562
+ }
2563
+ else {
2564
+ replay.answered = true;
2565
+ }
2566
+ }
2567
+ return outcome;
2568
+ };
2569
+ /** Confirms that a replayed message was answered by a report posted ahead of its own renderer. */
2570
+ const noteReplayAnsweredAhead = (connection, channel, ts) => {
2571
+ const replay = replaying.get(pendingKey({ agentId: connection.config.agentId, team: connection.teamId, channel, ts }));
2572
+ if (replay !== undefined) {
2573
+ replay.answered = true;
2574
+ }
2575
+ };
2576
+ /**
2577
+ * Live intake for a conversation (agent and channel) that has kept
2578
+ * messages waits here until each of them has taken its place on its
2579
+ * session's chain or been left for the next start, so a kept message is
2580
+ * never answered after one sent later in the same conversation. Other
2581
+ * conversations are not held. There is no timeout on the hold itself:
2582
+ * the bound is on each read (`REPLAY_READ_MAX_MS`), and a read that
2583
+ * outlasts it gives up its message for this run, which releases the hold.
2584
+ */
2585
+ const replayHolds = new Map();
2586
+ const holdKey = (agentId, channel) => `${agentId}:${channel}`;
2587
+ /**
2588
+ * Whether `connection` is the one to replay `entry` through: the same
2589
+ * agent in the workspace it was kept from. Channel and user ids belong to
2590
+ * a workspace, so an agent whose credentials moved to another one leaves
2591
+ * the entry kept (and its day-old expiry) rather than reading or answering
2592
+ * whatever has that id there.
2593
+ */
2594
+ const keptFor = (connection, entry) => connection.config.agentId === entry.agentId && connection.teamId === entry.team;
2595
+ const holdConversation = (key) => {
2596
+ if (replayHolds.has(key)) {
2597
+ return;
2598
+ }
2599
+ let release = () => { };
2600
+ const held = new Promise((resolve) => {
2601
+ release = () => {
2602
+ replayHolds.delete(key);
2603
+ resolve();
2604
+ };
2605
+ });
2606
+ replayHolds.set(key, { held, release });
2607
+ };
2608
+ const releaseConversation = (key) => replayHolds.get(key)?.release();
2609
+ /** Aborted by `stop()`, which ends a replay still reading kept messages back. */
2610
+ let replayStopping;
2611
+ const releaseAllConversations = () => {
2612
+ for (const { release } of [...replayHolds.values()]) {
2613
+ release();
2614
+ }
2615
+ };
2616
+ /** Keeps a refused message for the next start; resolves with whether it was kept. */
2617
+ const keepForNextStart = async (entry) => {
2618
+ if (pendingIntakePath === undefined) {
2619
+ return false;
2620
+ }
2621
+ try {
2622
+ // Every kept message was told it will be answered, so a full list
2623
+ // never evicts one of them: it makes room only from messages too old
2624
+ // to be answered at start anyway, and otherwise refuses this one, which
2625
+ // then gets the plain refusal and no promise.
2626
+ const { full, expired } = await changePending((entries) => {
2627
+ const key = pendingKey(entry);
2628
+ let others = entries.filter((existing) => pendingKey(existing) !== key);
2629
+ let expired = [];
2630
+ if (others.length >= PENDING_INTAKE_MAX) {
2631
+ const cutoff = now() - PENDING_INTAKE_MAX_AGE_MS;
2632
+ expired = others.filter((existing) => existing.keptAt < cutoff);
2633
+ others = others.filter((existing) => existing.keptAt >= cutoff);
2634
+ }
2635
+ if (others.length >= PENDING_INTAKE_MAX) {
2636
+ return { entries, result: { full: true, expired: [] } };
2637
+ }
2638
+ return { entries: [...others, entry], result: { full: false, expired } };
2639
+ });
2640
+ for (const dropped of expired) {
2641
+ log(`slack: ${dropped.agentId} dropped ${dropped.ts} in ${dropped.channel} kept from a restart: older than a day`);
2642
+ }
2643
+ if (full) {
2644
+ warn(`slack: could not keep ${entry.ts} in ${entry.channel} for after the restart: ${PENDING_INTAKE_MAX} messages are already kept`);
2645
+ return false;
2646
+ }
2647
+ const replay = replaying.get(pendingKey(entry));
2648
+ if (replay !== undefined) {
2649
+ replay.rekept = true;
2650
+ }
2651
+ return true;
2652
+ }
2653
+ catch (error) {
2654
+ warn(`slack: could not keep ${entry.ts} in ${entry.channel} for after the restart: ${error instanceof Error ? error.message : String(error)}`);
2655
+ return false;
2656
+ }
2657
+ };
2658
+ /** The kept message itself, as Slack has it now; `undefined` when it is gone. */
2659
+ const fetchKept = async (connection, entry) => {
2660
+ const { history, replies } = connection.web.conversations;
2661
+ const inThread = entry.threadTs !== undefined && entry.threadTs !== entry.ts;
2662
+ const range = { channel: entry.channel, oldest: entry.ts, latest: entry.ts, inclusive: true };
2663
+ const page = inThread
2664
+ ? await replies?.call(connection.web.conversations, { ...range, ts: entry.threadTs, limit: 2 })
2665
+ : await history?.call(connection.web.conversations, { ...range, limit: 1 });
2666
+ if (page === undefined) {
2667
+ throw new Error('this Slack client cannot read conversations');
2668
+ }
2669
+ return page.messages?.find((message) => message.ts === entry.ts);
2670
+ };
2671
+ /** One kept message read back with a bound; `timedOut` when Slack took longer than that. */
2672
+ const fetchKeptWithin = async (connection, entry) => {
2673
+ let timer;
2674
+ const late = new Promise((resolve) => {
2675
+ timer = setTimeout(() => resolve({ timedOut: true }), REPLAY_READ_MAX_MS);
2676
+ });
2677
+ try {
2678
+ return await Promise.race([
2679
+ fetchKept(connection, entry).then((message) => (message !== undefined ? { message } : {})),
2680
+ late,
2681
+ ]);
2682
+ }
2683
+ finally {
2684
+ clearTimeout(timer);
2685
+ }
2686
+ };
2687
+ /**
2688
+ * What the last daemon kept, handled as if it had just arrived: through
2689
+ * admission, the thread rules, and a dispatch keyed as the original
2690
+ * delivery would have been, so a message the gateway already ran is a
2691
+ * repeat there and not a second turn. That makes handing one over twice
2692
+ * safe and losing one not, so each comes off the list only once its
2693
+ * handler has reached the gateway and settled: a crash, or a failure
2694
+ * before the dispatch (the reply could not be started), leaves it for the
2695
+ * next start, and one kept again by a restart during the replay stays
2696
+ * kept. A message deleted since is dropped.
2697
+ *
2698
+ * Each conversation is replayed in ts order while its live intake is held
2699
+ * (`replayHolds`), and released once its kept messages are handed over.
2700
+ * A read that fails or outlasts `REPLAY_READ_MAX_MS` leaves that message,
2701
+ * and the ones kept after it in the conversation, for the next start:
2702
+ * handing a later one over first, or the slow one over after live
2703
+ * messages were let in, would answer them out of order.
2704
+ */
2705
+ const replayKept = async (kept, onEvent, stopping) => {
2706
+ if (pendingIntakePath === undefined || kept.length === 0) {
2707
+ return;
2708
+ }
2709
+ const cutoff = now() - PENDING_INTAKE_MAX_AGE_MS;
2710
+ const expired = kept.filter((entry) => entry.keptAt < cutoff);
2711
+ for (const entry of expired) {
2712
+ log(`slack: ${entry.agentId} dropped ${entry.ts} in ${entry.channel} kept from a restart: older than a day`);
2713
+ }
2714
+ // Not waited on before the reads: the list's writes are serialized, so
2715
+ // this one lands ahead of any the replay makes, and the reads start
2716
+ // inside start() (a stop right after it still sees each conversation's
2717
+ // first one through).
2718
+ const old = new Set(expired.map(pendingKey));
2719
+ const droppingExpired = expired.length > 0
2720
+ ? changePending((entries) => ({ entries: entries.filter((entry) => !old.has(pendingKey(entry))), result: undefined }))
2721
+ .catch((error) => warn(`slack: could not drop the day-old messages kept from a restart; the next start drops them: ${error instanceof Error ? error.message : String(error)}`))
2722
+ : Promise.resolve();
2723
+ const conversations = new Map();
2724
+ for (const entry of kept.filter((candidate) => candidate.keptAt >= cutoff)) {
2725
+ const elsewhere = connections.find((connection) => connection.config.agentId === entry.agentId && !keptFor(connection, entry));
2726
+ if (elsewhere !== undefined) {
2727
+ warn(`slack: ${entry.agentId} kept ${entry.ts} in ${entry.channel} from workspace ${entry.team}, but is now connected to ${elsewhere.teamId}; keeping it for a start in that workspace`);
2728
+ continue;
2729
+ }
2730
+ const key = holdKey(entry.agentId, entry.channel);
2731
+ conversations.set(key, [...(conversations.get(key) ?? []), entry]);
2732
+ }
2733
+ const handovers = [];
2734
+ const replayConversation = async (key, entries) => {
2735
+ try {
2736
+ entries.sort((a, b) => compareSlackTs(a.ts, b.ts));
2737
+ const agentId = entries[0].agentId;
2738
+ const connection = connections.find((candidate) => keptFor(candidate, entries[0]));
2739
+ const handle = onEvent.get(agentId);
2740
+ if (connection === undefined || handle === undefined) {
2741
+ return;
2742
+ }
2743
+ for (const [index, entry] of entries.entries()) {
2744
+ // A stop ends the replay at the next message: what it has not read
2745
+ // yet stays kept for the next start, so the stop waits on at most
2746
+ // the one bounded read in progress per conversation, not a chain
2747
+ // of them.
2748
+ if (stopping.aborted) {
2749
+ log(`slack: ${agentId} stopping; leaving ${entries.length - index} kept message(s) in ${entry.channel} for the next start`);
2750
+ return;
2751
+ }
2752
+ let read;
2753
+ try {
2754
+ read = await fetchKeptWithin(connection, entry);
2755
+ }
2756
+ catch (error) {
2757
+ warn(`slack: could not read ${entry.ts} in ${entry.channel} kept from the last restart; keeping it and ${entries.length - index - 1} after it for the next start: ${error instanceof Error ? error.message : String(error)}`);
2758
+ return;
2759
+ }
2760
+ if (read.timedOut === true) {
2761
+ warn(`slack: reading ${entry.ts} in ${entry.channel} kept from the last restart took over ${REPLAY_READ_MAX_MS / 1000}s; keeping it and ${entries.length - index - 1} after it for the next start`);
2762
+ return;
2763
+ }
2764
+ const message = read.message;
2765
+ if (message === undefined) {
2766
+ const gone = pendingKey(entry);
2767
+ await changePending((current) => ({ entries: current.filter((kept) => pendingKey(kept) !== gone), result: undefined }));
2768
+ log(`slack: ${entry.agentId} dropped ${entry.ts} in ${entry.channel} kept from a restart: no longer in Slack`);
2769
+ continue;
2770
+ }
2771
+ log(`slack: ${entry.agentId} replaying ${entry.ts} in ${entry.channel}, sent during the last restart`);
2772
+ const event = {
2773
+ ...message,
2774
+ type: 'message',
2775
+ channel: entry.channel,
2776
+ ts: entry.ts,
2777
+ ...(entry.threadTs !== undefined ? { thread_ts: entry.threadTs } : {}),
2778
+ ...(entry.channelType !== undefined ? { channel_type: entry.channelType } : {}),
2779
+ };
2780
+ const replayKey = pendingKey(entry);
2781
+ const replay = { dispatched: false, rekept: false, refused: false, answered: false };
2782
+ replaying.set(replayKey, replay);
2783
+ // Not waited on: the handler claims its place on its session's
2784
+ // chain synchronously, as a live delivery does, so the next kept
2785
+ // message (and, after the last, the held live ones) queue behind it.
2786
+ const handled = handle({ ack: async () => { }, body: { team_id: entry.team }, event });
2787
+ handovers.push(handled.catch(() => undefined).then(async () => {
2788
+ replaying.delete(replayKey);
2789
+ if (replay.rekept) {
2790
+ return;
2791
+ }
2792
+ if (!replay.dispatched) {
2793
+ warn(`slack: ${entry.agentId} could not hand ${entry.ts} in ${entry.channel} kept from a restart to the gateway; keeping it for the next start`);
2794
+ return;
2795
+ }
2796
+ if (replay.refused) {
2797
+ warn(`slack: ${entry.agentId} could not post the answer to ${entry.ts} in ${entry.channel} kept from a restart; keeping it for the next start`);
2798
+ return;
2799
+ }
2800
+ if (!replay.answered) {
2801
+ warn(`slack: ${entry.agentId} has no confirmed answer to ${entry.ts} in ${entry.channel} kept from a restart; keeping it for the next start`);
2802
+ return;
2803
+ }
2804
+ await changePending((current) => ({ entries: current.filter((kept) => pendingKey(kept) !== replayKey), result: undefined }));
2805
+ }));
2806
+ }
2807
+ }
2808
+ finally {
2809
+ releaseConversation(key);
2810
+ }
2811
+ };
2812
+ await Promise.all([...conversations].map(([key, entries]) => replayConversation(key, entries)));
2813
+ await Promise.all(handovers);
2814
+ await droppingExpired;
2815
+ };
2425
2816
  const alreadySeen = (key) => {
2426
2817
  if (seenEvents.has(key)) {
2427
2818
  return true;
@@ -3660,6 +4051,11 @@ export const createSlackChannelAdapter = (options) => {
3660
4051
  return landed;
3661
4052
  };
3662
4053
  const reportUnrenderedFailure = async (event, behind, files) => {
4054
+ // A turn no renderer here was showing (card 501) that a Slack stop
4055
+ // aborted fails with the stop's own reason, so who stopped it is read
4056
+ // from the failure itself rather than from a note an outcome may never
4057
+ // come to clear.
4058
+ const stoppedBy = slackStopper(event.error);
3663
4059
  const gateway = gatewayRef;
3664
4060
  if (!gateway?.sessionRouting) {
3665
4061
  return NOTHING_REPORTED;
@@ -3687,7 +4083,10 @@ export const createSlackChannelAdapter = (options) => {
3687
4083
  }
3688
4084
  const channel = metadata.slackChannel;
3689
4085
  const thread = typeof metadata.slackThread === 'string' ? metadata.slackThread : undefined;
3690
- if (routing.unaddressed === true && routing.reply === undefined) {
4086
+ if (stoppedBy === undefined && routing.unaddressed === true && routing.reply === undefined) {
4087
+ // A stop is someone asking, so a stopped turn always says who
4088
+ // stopped it, judged or not.
4089
+ //
3691
4090
  // A turn nobody asked for that said nothing before it broke — the
3692
4091
  // daemon died inside it, and the next start failed it with no
3693
4092
  // renderer here to keep quiet for it. The same rule as the lazy
@@ -3708,7 +4107,7 @@ export const createSlackChannelAdapter = (options) => {
3708
4107
  return { said: true, landed: await uploadUnrenderedFiles(connection, channel, thread, files) };
3709
4108
  }
3710
4109
  let landed = [];
3711
- const said = await postAheadOf(behind, connection, channel, thread, messageChunks(`Something went wrong: ${event.error}`), async () => {
4110
+ const said = await postAheadOf(behind, connection, channel, thread, messageChunks(stoppedBy === undefined ? `Something went wrong: ${event.error}` : `Stopped by <@${stoppedBy}>.`), async () => {
3712
4111
  landed = await uploadUnrenderedFiles(connection, channel, thread, files);
3713
4112
  });
3714
4113
  return { said, landed };
@@ -4137,7 +4536,7 @@ export const createSlackChannelAdapter = (options) => {
4137
4536
  * Dropping that one would hand the thread back to its owner after enough
4138
4537
  * mentions of a third agent, while the judge that took it still answers.
4139
4538
  */
4140
- const trimHandovers = (handovers) => {
4539
+ const trimHandovers = (key, handovers) => {
4141
4540
  if (handovers.length <= THREAD_HANDOVER_DEPTH) {
4142
4541
  return handovers;
4143
4542
  }
@@ -4153,6 +4552,13 @@ export const createSlackChannelAdapter = (options) => {
4153
4552
  // The kept take sits on top of the full recent tail, one past the depth,
4154
4553
  // so `holderAt` still sees every recent claim it saw before.
4155
4554
  const tail = handovers.slice(tailFrom);
4555
+ // The cut only ever moves later: a claim inserted into the gap since
4556
+ // is dropped again, and the gap it fell in stays unknown.
4557
+ const until = tail[0].ts;
4558
+ const before = threadTrimmedUntil.get(key);
4559
+ if (before === undefined || before < until) {
4560
+ threadTrimmedUntil.set(key, until);
4561
+ }
4156
4562
  return latestTake > 0 && latestTake < tailFrom
4157
4563
  ? [first, handovers[latestTake], ...tail]
4158
4564
  : [first, ...tail];
@@ -4205,11 +4611,13 @@ export const createSlackChannelAdapter = (options) => {
4205
4611
  // Re-inserted rather than mutated in place: a Map iterates in insertion
4206
4612
  // order, which is what makes the first key the one to evict.
4207
4613
  threadAddressee.delete(key);
4208
- threadAddressee.set(key, trimHandovers(handovers));
4614
+ threadAddressee.set(key, trimHandovers(key, handovers));
4209
4615
  if (threadAddressee.size > THREAD_ADDRESSEE_CAPACITY) {
4210
4616
  const oldest = threadAddressee.keys().next();
4211
4617
  if (!oldest.done) {
4212
4618
  threadAddressee.delete(oldest.value);
4619
+ threadTrimmedUntil.delete(oldest.value);
4620
+ threadReadUntil.delete(oldest.value);
4213
4621
  // The owner goes with the record of who took the thread since: an
4214
4622
  // owner kept alone would stand in for a judge's takeover it no
4215
4623
  // longer knows of, where the thread read would see it.
@@ -4231,9 +4639,48 @@ export const createSlackChannelAdapter = (options) => {
4231
4639
  * fresh read of the thread, never sent to the owner on a guess.
4232
4640
  */
4233
4641
  const stopHolderAt = (key, ts) => holderAt(key, ts);
4642
+ /** Whether the record of thread `key` lost who held it at `ts` to a cut. */
4643
+ const inTrimmedGap = (key, handovers, ts) => {
4644
+ const until = threadTrimmedUntil.get(key);
4645
+ const first = handovers[0];
4646
+ return until !== undefined && first !== undefined && first.ts < ts && ts < until;
4647
+ };
4648
+ /**
4649
+ * Records that a thread read of `key` covered every message before `ts`.
4650
+ * Bounded on its own, since a read that found nobody's post leaves an
4651
+ * owner and no record (Codex 4321103567). A thread whose mark is evicted
4652
+ * loses its record and owner with it, so it is read afresh instead of
4653
+ * placed from claims the read may have stopped short of.
4654
+ */
4655
+ const noteReadUntil = (key, ts) => {
4656
+ const until = threadReadUntil.get(key);
4657
+ threadReadUntil.delete(key);
4658
+ threadReadUntil.set(key, until !== undefined && compareSlackTs(ts, until) < 0 ? until : ts);
4659
+ if (threadReadUntil.size > THREAD_ADDRESSEE_CAPACITY) {
4660
+ const oldest = threadReadUntil.keys().next();
4661
+ if (!oldest.done) {
4662
+ threadReadUntil.delete(oldest.value);
4663
+ threadAddressee.delete(oldest.value);
4664
+ threadTrimmedUntil.delete(oldest.value);
4665
+ threadOrigins.delete(oldest.value);
4666
+ }
4667
+ }
4668
+ };
4669
+ /**
4670
+ * Where the record of thread `key` stops knowing every take, for a
4671
+ * message at `ts` after what a thread read covered (card 562): a claim
4672
+ * older than the returned point may have been taken from since, unseen.
4673
+ * Undefined when the record is complete as far as `ts`.
4674
+ */
4675
+ const unreadBefore = (key, ts) => {
4676
+ const until = threadReadUntil.get(key);
4677
+ return until !== undefined && liveFrom !== undefined && compareSlackTs(until, liveFrom) < 0 && compareSlackTs(ts, until) > 0
4678
+ ? liveFrom
4679
+ : undefined;
4680
+ };
4234
4681
  const holderAt = (key, ts) => {
4235
4682
  const handovers = threadAddressee.get(key);
4236
- if (!handovers) {
4683
+ if (!handovers || inTrimmedGap(key, handovers, ts)) {
4237
4684
  return undefined;
4238
4685
  }
4239
4686
  // The newest entry at or before `ts` answers, and only if this process
@@ -4241,10 +4688,11 @@ export const createSlackChannelAdapter = (options) => {
4241
4688
  // asked since may be a mention it cannot see, so it leaves the question
4242
4689
  // to a fresh read. And it is newer than every live entry before it, so
4243
4690
  // skipping it for one of those would answer with a claim it superseded.
4691
+ const unread = unreadBefore(key, ts);
4244
4692
  for (let index = handovers.length - 1; index >= 0; index -= 1) {
4245
4693
  const handover = handovers[index];
4246
4694
  if (handover && handover.ts <= ts) {
4247
- return handover.cold === true ? undefined : handover.agentId;
4695
+ return handover.cold === true || (unread !== undefined && compareSlackTs(handover.ts, unread) < 0) ? undefined : handover.agentId;
4248
4696
  }
4249
4697
  }
4250
4698
  return undefined;
@@ -4266,13 +4714,19 @@ export const createSlackChannelAdapter = (options) => {
4266
4714
  const ownerAt = (key, ts) => {
4267
4715
  const origin = threadOrigins.get(key);
4268
4716
  const handovers = threadAddressee.get(key) ?? [];
4717
+ if (inTrimmedGap(key, handovers, ts)) {
4718
+ return undefined;
4719
+ }
4720
+ // A take from before the stretch the read left out may have been
4721
+ // superseded there: the question goes to a fresh read (card 562).
4722
+ const unread = unreadBefore(key, ts);
4269
4723
  for (let index = handovers.length - 1; index >= 0; index -= 1) {
4270
4724
  const handover = handovers[index];
4271
4725
  if (handover && handover.ts <= ts && (handover.takes === true || (index === 0 && typeof origin !== 'string'))) {
4272
- return handover.agentId;
4726
+ return unread !== undefined && compareSlackTs(handover.ts, unread) < 0 ? undefined : handover.agentId;
4273
4727
  }
4274
4728
  }
4275
- return typeof origin === 'string' ? origin : undefined;
4729
+ return typeof origin === 'string' && unread === undefined ? origin : undefined;
4276
4730
  };
4277
4731
  /**
4278
4732
  * The agent the thread `key` belongs to, whoever holds it now: the owner
@@ -4798,6 +5252,7 @@ export const createSlackChannelAdapter = (options) => {
4798
5252
  for (const post of takeovers) {
4799
5253
  rememberAddressee(key, post.agentId, post.ts, true, true);
4800
5254
  }
5255
+ noteReadUntil(key, ts);
4801
5256
  };
4802
5257
  if (taken && taken.agentId !== ownerId && (ownerId !== undefined || taken.at > read.foreign)) {
4803
5258
  warm();
@@ -4925,19 +5380,30 @@ export const createSlackChannelAdapter = (options) => {
4925
5380
  const stopTurns = (connection, sessionId, userId, channel, fromIntake = false) => {
4926
5381
  const running = renderers.get(sessionId) ?? [];
4927
5382
  const inIntake = fromIntake ? 0 : intakeInFlight.get(sessionId) ?? 0;
4928
- if (running.length === 0 && inIntake === 0) {
4929
- return false;
4930
- }
5383
+ const reason = `${SLACK_STOP_REASON}${userId}`;
4931
5384
  for (const renderer of running) {
4932
5385
  if (renderer.stoppedBy === undefined) {
4933
5386
  renderer.stoppedBy = userId;
4934
- renderer.controller.abort(new RunAbortedError(`Stopped from Slack by ${userId}`));
4935
- }
5387
+ renderer.controller.abort(new RunAbortedError(reason));
5388
+ }
5389
+ }
5390
+ // Also a turn no renderer of this process started (card 501): one the
5391
+ // gateway recovered after a restart, or another caller's on this
5392
+ // session. The gateway counts every turn still on the session, one
5393
+ // already stopping included, so a repeated stop while a stopped turn
5394
+ // winds down is still a stop, and once it has ended, however it ended,
5395
+ // a stop with nothing running is an ordinary message. The renderers'
5396
+ // started turns are among those counted.
5397
+ const onSession = gatewayRef?.abortTurns?.(sessionId, reason) ?? 0;
5398
+ if (running.length === 0 && inIntake === 0 && onSession === 0) {
5399
+ return false;
4936
5400
  }
4937
5401
  if (inIntake > 0) {
4938
5402
  stopsBySession.set(sessionId, { count: (stopsBySession.get(sessionId)?.count ?? 0) + 1, by: userId });
4939
5403
  }
4940
- log(`slack: ${userId} stopped ${running.length + inIntake} turn(s) of ${connection.config.agentId} in ${channel}`);
5404
+ // A renderer's started turn is also one the gateway counts, so this is
5405
+ // the larger of the two, not their sum.
5406
+ log(`slack: ${userId} stopped ${Math.max(running.length, onSession) + inIntake} turn(s) of ${connection.config.agentId} in ${channel}`);
4941
5407
  return true;
4942
5408
  };
4943
5409
  /**
@@ -5698,7 +6164,7 @@ export const createSlackChannelAdapter = (options) => {
5698
6164
  leaveIntake();
5699
6165
  if (stopped !== undefined && stopped.count !== stopsAtClaim && renderer.stoppedBy === undefined) {
5700
6166
  renderer.stoppedBy = stopped.by;
5701
- renderer.controller.abort(new RunAbortedError(`Stopped from Slack by ${stopped.by}`));
6167
+ renderer.controller.abort(new RunAbortedError(`${SLACK_STOP_REASON}${stopped.by}`));
5702
6168
  }
5703
6169
  // In a DM each message is its own status key, and Slack stops showing
5704
6170
  // the running turn's once a newer message sits below it — so a
@@ -5708,6 +6174,10 @@ export const createSlackChannelAdapter = (options) => {
5708
6174
  renderer.showLoading(queue.length === 1 || queue[0]?.statusThread !== renderer.statusThread);
5709
6175
  let repeat;
5710
6176
  let absorbed = false;
6177
+ const replay = replaying.get(pendingKey({ agentId: connection.config.agentId, team: connection.teamId, channel: event.channel, ts: event.ts }));
6178
+ if (replay !== undefined) {
6179
+ replay.dispatched = true;
6180
+ }
5711
6181
  const turn = gateway.dispatch({
5712
6182
  sessionId,
5713
6183
  agentId: connection.config.agentId,
@@ -5727,10 +6197,16 @@ export const createSlackChannelAdapter = (options) => {
5727
6197
  },
5728
6198
  // Sent while a turn is running, this message is answered by the
5729
6199
  // next turn together with everything else that queued (card 423);
5730
- // the first of them renders that answer.
5731
- onAbsorbed: () => {
5732
- absorbed = true;
5733
- },
6200
+ // the first of them renders that answer. Not a replayed message:
6201
+ // it leaves the kept list only on its own confirmed answer, and an
6202
+ // answer another message's renderer posts is not one it can see.
6203
+ ...(replay === undefined
6204
+ ? {
6205
+ onAbsorbed: () => {
6206
+ absorbed = true;
6207
+ },
6208
+ }
6209
+ : {}),
5734
6210
  });
5735
6211
  return { renderer, turn, repeat: () => repeat, absorbed: () => absorbed };
5736
6212
  });
@@ -5783,6 +6259,15 @@ export const createSlackChannelAdapter = (options) => {
5783
6259
  // Likewise a message absorbed into a merged turn: the turn's leading
5784
6260
  // message's renderer answers for all of them.
5785
6261
  if (repeat() === 'live' || absorbed()) {
6262
+ // A replayed message whose turn the start-up recovery owns is
6263
+ // answered by that recovery's report, and only if it reached the
6264
+ // thread; anything else leaves it kept.
6265
+ const ahead = repeat() === 'live' && renderer.outcomeReportedAhead !== undefined
6266
+ ? await renderer.outcomeReportedAhead
6267
+ : undefined;
6268
+ if (ahead?.said === true) {
6269
+ noteReplayAnsweredAhead(connection, event.channel, event.ts);
6270
+ }
5786
6271
  await renderer.withdraw();
5787
6272
  renderers.get(sessionId)?.[0]?.refreshLoading();
5788
6273
  return;
@@ -5820,6 +6305,7 @@ export const createSlackChannelAdapter = (options) => {
5820
6305
  }));
5821
6306
  }
5822
6307
  if (reportedAhead?.said === true) {
6308
+ noteReplayAnsweredAhead(connection, event.channel, event.ts);
5823
6309
  await renderer.withdraw();
5824
6310
  renderers.get(sessionId)?.[0]?.refreshLoading();
5825
6311
  return;
@@ -5852,6 +6338,7 @@ export const createSlackChannelAdapter = (options) => {
5852
6338
  // interim messages already in the thread are heard either way.
5853
6339
  let replyLanded = false;
5854
6340
  const settled = finalized.then((outcome) => {
6341
+ noteReplayOutcome(connection, event.channel, event.ts, outcome);
5855
6342
  replyLanded = outcome.published;
5856
6343
  return true;
5857
6344
  }, () => true);
@@ -5890,11 +6377,48 @@ export const createSlackChannelAdapter = (options) => {
5890
6377
  // not a failure of the agent. Matched by name, as this package does
5891
6378
  // not import the gateway. A turn nobody asked for stays silent.
5892
6379
  const pausedReply = failure instanceof Error && failure.name === 'IntakePausedError';
5893
- const { spoke, spokeAt } = renderer.stoppedBy !== undefined
6380
+ // A daemon on its way down that still heard this (the gateway keeps
6381
+ // channels connected through its drain): the message is kept for the
6382
+ // next start, which handles it as if it had just arrived. Said only
6383
+ // to a message that asked: a judged reply in the agent's thread (a
6384
+ // turn nobody asked for) is kept so the next daemon can still judge
6385
+ // it, but nobody asked, so a notice under it would be noise. An
6386
+ // overheard message never gets here; it was never going to be a turn.
6387
+ // Matched by name, like the pause.
6388
+ const stoppingRefusal = failure instanceof Error && failure.name === 'GatewayStoppingError' && renderer.stoppedBy === undefined;
6389
+ let deferredText;
6390
+ if (stoppingRefusal) {
6391
+ const restarting = failure.restarting !== false;
6392
+ const kept = await keepForNextStart({
6393
+ agentId: connection.config.agentId,
6394
+ team,
6395
+ channel: event.channel,
6396
+ ts: event.ts,
6397
+ ...(event.thread_ts !== undefined ? { threadTs: event.thread_ts } : {}),
6398
+ user: userId,
6399
+ ...(event.channel_type !== undefined ? { channelType: event.channel_type } : {}),
6400
+ keptAt: now(),
6401
+ });
6402
+ if (kept) {
6403
+ log(`slack: ${connection.config.agentId} kept ${event.ts} in ${event.channel} to answer after the ${restarting ? 'restart' : 'stop'}`);
6404
+ deferredText = restarting ? RESTARTING_REPLY_TEXT : STOPPING_REPLY_TEXT;
6405
+ }
6406
+ else {
6407
+ deferredText = failure.message;
6408
+ }
6409
+ }
6410
+ // A refusal (paused, or a daemon on its way down) ran no turn, so its
6411
+ // notice is never what answers a replayed message: that one stays
6412
+ // kept, whether or not keeping it again just worked.
6413
+ const refused = pausedReply || stoppingRefusal;
6414
+ const outcome = renderer.stoppedBy !== undefined
5894
6415
  ? await renderer.stopped(renderer.stoppedBy)
5895
- : pausedReply
5896
- ? await renderer.finalize(renderer.lazy ? '' : failure.message)
6416
+ : refused
6417
+ ? await renderer.finalize(renderer.lazy ? '' : deferredText ?? failure.message)
5897
6418
  : await renderer.fail(failure instanceof Error ? failure.message : String(failure));
6419
+ const { spoke, spokeAt } = refused && renderer.stoppedBy === undefined
6420
+ ? outcome
6421
+ : noteReplayOutcome(connection, event.channel, event.ts, outcome);
5898
6422
  if (spoke && threadKey !== undefined) {
5899
6423
  // A file it posted before breaking has still spoken, by the same
5900
6424
  // rule as a reply that landed.
@@ -5915,6 +6439,10 @@ export const createSlackChannelAdapter = (options) => {
5915
6439
  requestCredential,
5916
6440
  async start(gateway) {
5917
6441
  gatewayRef = gateway;
6442
+ // Nothing counts as seen live until every socket is up (Codex
6443
+ // 4321128637): a message sent while the sockets are still coming up
6444
+ // reaches none of them, so a read stopping before it stops short.
6445
+ liveFrom = NOT_LIVE_YET;
5918
6446
  unsubscribe = gateway.bus.subscribe((event) => {
5919
6447
  // Approval traffic is not part of a reply: it gets its own message
5920
6448
  // with its own lifetime, and routing it through the running turn's
@@ -6066,21 +6594,55 @@ export const createSlackChannelAdapter = (options) => {
6066
6594
  }
6067
6595
  }
6068
6596
  settleHomeOwners();
6597
+ const listeners = new Map();
6598
+ // Read before any socket delivers, so each conversation with kept
6599
+ // messages is held from the first live event on.
6600
+ let kept = [];
6601
+ if (pendingIntakePath !== undefined) {
6602
+ try {
6603
+ kept = await readPending(pendingIntakePath);
6604
+ }
6605
+ catch (error) {
6606
+ warn(`slack: could not read the messages kept from the last restart: ${error instanceof Error ? error.message : String(error)}`);
6607
+ }
6608
+ const cutoff = now() - PENDING_INTAKE_MAX_AGE_MS;
6609
+ for (const entry of kept) {
6610
+ if (entry.keptAt >= cutoff && authenticated.some((connection) => keptFor(connection, entry))) {
6611
+ holdConversation(holdKey(entry.agentId, entry.channel));
6612
+ }
6613
+ }
6614
+ }
6069
6615
  for (const connection of authenticated) {
6070
6616
  const { config, socket } = connection;
6071
6617
  try {
6072
6618
  // Returns the work it started: the SDK ignores it, and a test's fake
6073
6619
  // socket waits on it rather than guessing how long it takes.
6074
- const onEvent = (args) => {
6075
- const handled = handleInbound(connection, args).catch((error) => {
6620
+ const handle = (inbound) => {
6621
+ const handled = inbound.catch((error) => {
6076
6622
  warn(`slack event handling failed: ${error instanceof Error ? error.message : String(error)}`);
6077
6623
  });
6078
6624
  inflight.add(handled);
6079
6625
  void handled.finally(() => inflight.delete(handled));
6080
6626
  return handled;
6081
6627
  };
6628
+ // The replay's own deliveries never wait on the hold they release.
6629
+ const onReplay = (args) => handle(handleInbound(connection, args));
6630
+ const onEvent = (args) => {
6631
+ const channel = args.event?.channel;
6632
+ const hold = typeof channel === 'string' ? replayHolds.get(holdKey(config.agentId, channel))?.held : undefined;
6633
+ if (hold === undefined) {
6634
+ return onReplay(args);
6635
+ }
6636
+ // Acked now, as any delivery is, so Slack does not redeliver a
6637
+ // message that is only waiting its turn; admitted after the hold.
6638
+ void Promise.resolve(args.ack()).catch((error) => {
6639
+ warn(`slack: could not ack an envelope: ${error instanceof Error ? error.message : String(error)}`);
6640
+ });
6641
+ return handle(hold.then(() => handleInbound(connection, { ...args, ack: async () => { } })));
6642
+ };
6082
6643
  socket.on('app_mention', onEvent);
6083
6644
  socket.on('message', onEvent);
6645
+ listeners.set(config.agentId, onReplay);
6084
6646
  socket.on('interactive', (args) => {
6085
6647
  const handled = handleInteractive(connection, args).catch((error) => {
6086
6648
  warn(`slack interaction handling failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -6098,8 +6660,19 @@ export const createSlackChannelAdapter = (options) => {
6098
6660
  warn(`slack: could not connect ${config.agentId}: ${error instanceof Error ? error.message : String(error)}`);
6099
6661
  }
6100
6662
  }
6663
+ const liveMs = now() + LIVE_FROM_MARGIN_MS;
6664
+ liveFrom = `${Math.floor(liveMs / 1000)}.${String((liveMs % 1000) * 1000).padStart(6, '0')}`;
6665
+ // Once every app is connected, so a kept message is handled with the
6666
+ // whole roster's identities known, as a live one would be. Only for the
6667
+ // apps that did connect: the rest stay kept for a start where they do.
6668
+ // A kept message is from before the restart, so its thread read stops
6669
+ // short of the start, as `liveFrom` already says (card 562).
6670
+ replayStopping = new AbortController();
6671
+ track(replayKept(kept, new Map([...listeners].filter(([agentId]) => connections.some((connection) => connection.config.agentId === agentId))), replayStopping.signal).finally(releaseAllConversations));
6101
6672
  },
6102
6673
  async stop() {
6674
+ replayStopping?.abort();
6675
+ releaseAllConversations();
6103
6676
  // Stop intake first, so no new INBOUND event can slip into
6104
6677
  // `inflight` while the drain below runs.
6105
6678
  await Promise.allSettled(connections.map((connection) => connection.socket.disconnect()));
@@ -6127,6 +6700,8 @@ export const createSlackChannelAdapter = (options) => {
6127
6700
  connections.length = 0;
6128
6701
  approvalPosts.clear();
6129
6702
  threadAddressee.clear();
6703
+ threadTrimmedUntil.clear();
6704
+ threadReadUntil.clear();
6130
6705
  senderAddressees.clear();
6131
6706
  coldVerdicts.clear();
6132
6707
  botIdentities.clear();