instar 1.3.597 → 1.3.598
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/core/PlacementResponderReader.d.ts +45 -0
- package/dist/core/PlacementResponderReader.d.ts.map +1 -0
- package/dist/core/PlacementResponderReader.js +46 -0
- package/dist/core/PlacementResponderReader.js.map +1 -0
- package/dist/core/SlackLiveSender.d.ts +65 -0
- package/dist/core/SlackLiveSender.d.ts.map +1 -0
- package/dist/core/SlackLiveSender.js +77 -0
- package/dist/core/SlackLiveSender.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.598.md +24 -0
- package/upgrades/side-effects/live-test-slack-sender.md +29 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlacementResponderReader — resolves WHICH machine served a reply, for the
|
|
3
|
+
* RealChannelDriver's injected `resolveResponderMachine` seam
|
|
4
|
+
* (docs/specs/live-user-channel-proof-standard.md §5.6 — the deterministic
|
|
5
|
+
* cross-machine proof). It maps a (surface, channelId) to the topic that
|
|
6
|
+
* `GET /pool/placement?topic=N` is keyed on, reads the authoritative owner, and
|
|
7
|
+
* returns that machine id.
|
|
8
|
+
*
|
|
9
|
+
* Why this is the right reader: `/pool/placement` is the authoritative placement
|
|
10
|
+
* surface (it proxies to the lease-holder, so any machine can answer), and its
|
|
11
|
+
* `owner` field is the machine currently serving the topic — exactly "who replied"
|
|
12
|
+
* after a transfer. We read `owner` (not `pinnedTo`): a pin is the INTENT to place;
|
|
13
|
+
* `owner` is who actually holds it. For the capstone that distinction is the whole
|
|
14
|
+
* point — a pin that didn't move the seat is the original bug.
|
|
15
|
+
*
|
|
16
|
+
* Parameterized for testability: the HTTP GET + the (surface, channelId)→topicId
|
|
17
|
+
* mapping are injected, so this is unit-testable with no live server. MUST NOT throw
|
|
18
|
+
* on a transient read error — it returns null so the RealChannelDriver degrades to an
|
|
19
|
+
* unattributed reply rather than failing the whole scenario.
|
|
20
|
+
*/
|
|
21
|
+
import type { Surface } from './LiveTestArtifactStore.js';
|
|
22
|
+
export interface PlacementResult {
|
|
23
|
+
owner: string | null;
|
|
24
|
+
ownerNickname?: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface PlacementResponderReaderDeps {
|
|
27
|
+
/**
|
|
28
|
+
* Map (surface, channelId) → the topic id `/pool/placement` is keyed on. For
|
|
29
|
+
* telegram, channelId IS the topic. For slack, the caller injects the
|
|
30
|
+
* channel→topic resolution (the SlackAdapter's session keying). Return null when
|
|
31
|
+
* the channel doesn't map to a placement-tracked topic.
|
|
32
|
+
*/
|
|
33
|
+
topicForChannel: (surface: Surface, channelId: string) => string | null;
|
|
34
|
+
/** GET /pool/placement?topic=<id> → the placement (owner). Throwing/transient errors are tolerated. */
|
|
35
|
+
fetchPlacement: (topicId: string) => Promise<PlacementResult | null>;
|
|
36
|
+
logger?: (m: string) => void;
|
|
37
|
+
}
|
|
38
|
+
export declare class PlacementResponderReader {
|
|
39
|
+
private readonly d;
|
|
40
|
+
constructor(deps: PlacementResponderReaderDeps);
|
|
41
|
+
private log;
|
|
42
|
+
/** The function shape RealChannelDriver injects: (surface, channelId) → machine id | null. */
|
|
43
|
+
resolve: (surface: Surface, channelId: string) => Promise<string | null>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=PlacementResponderReader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PlacementResponderReader.d.ts","sourceRoot":"","sources":["../../src/core/PlacementResponderReader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAQH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,4BAA4B;IAC3C;;;;;OAKG;IACH,eAAe,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACxE,uGAAuG;IACvG,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;IACrE,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,qBAAa,wBAAwB;IACnC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAA+B;gBACrC,IAAI,EAAE,4BAA4B;IAE9C,OAAO,CAAC,GAAG;IAEX,8FAA8F;IAC9F,OAAO,GAAU,SAAS,OAAO,EAAE,WAAW,MAAM,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiB3E;CACH"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlacementResponderReader — resolves WHICH machine served a reply, for the
|
|
3
|
+
* RealChannelDriver's injected `resolveResponderMachine` seam
|
|
4
|
+
* (docs/specs/live-user-channel-proof-standard.md §5.6 — the deterministic
|
|
5
|
+
* cross-machine proof). It maps a (surface, channelId) to the topic that
|
|
6
|
+
* `GET /pool/placement?topic=N` is keyed on, reads the authoritative owner, and
|
|
7
|
+
* returns that machine id.
|
|
8
|
+
*
|
|
9
|
+
* Why this is the right reader: `/pool/placement` is the authoritative placement
|
|
10
|
+
* surface (it proxies to the lease-holder, so any machine can answer), and its
|
|
11
|
+
* `owner` field is the machine currently serving the topic — exactly "who replied"
|
|
12
|
+
* after a transfer. We read `owner` (not `pinnedTo`): a pin is the INTENT to place;
|
|
13
|
+
* `owner` is who actually holds it. For the capstone that distinction is the whole
|
|
14
|
+
* point — a pin that didn't move the seat is the original bug.
|
|
15
|
+
*
|
|
16
|
+
* Parameterized for testability: the HTTP GET + the (surface, channelId)→topicId
|
|
17
|
+
* mapping are injected, so this is unit-testable with no live server. MUST NOT throw
|
|
18
|
+
* on a transient read error — it returns null so the RealChannelDriver degrades to an
|
|
19
|
+
* unattributed reply rather than failing the whole scenario.
|
|
20
|
+
*/
|
|
21
|
+
export class PlacementResponderReader {
|
|
22
|
+
d;
|
|
23
|
+
constructor(deps) { this.d = deps; }
|
|
24
|
+
log(m) { this.d.logger?.(`[placement-responder-reader] ${m}`); }
|
|
25
|
+
/** The function shape RealChannelDriver injects: (surface, channelId) → machine id | null. */
|
|
26
|
+
resolve = async (surface, channelId) => {
|
|
27
|
+
const topicId = this.d.topicForChannel(surface, channelId);
|
|
28
|
+
if (!topicId) {
|
|
29
|
+
this.log(`no placement-tracked topic for ${surface}:${channelId}`);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const p = await this.d.fetchPlacement(topicId);
|
|
34
|
+
return p?.owner ?? null;
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
// @silent-fallback-ok: a transient /pool/placement read failure degrades this
|
|
38
|
+
// best-effort responder-machine lookup to null — the RealChannelDriver then
|
|
39
|
+
// proceeds with an unattributed reply rather than throwing the whole scenario.
|
|
40
|
+
// The failure is logged with topic context; it is not a gating/authority path.
|
|
41
|
+
this.log(`fetchPlacement failed for topic ${topicId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=PlacementResponderReader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PlacementResponderReader.js","sourceRoot":"","sources":["../../src/core/PlacementResponderReader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AA4BH,MAAM,OAAO,wBAAwB;IAClB,CAAC,CAA+B;IACjD,YAAY,IAAkC,IAAI,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAE1D,GAAG,CAAC,CAAS,IAAU,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,gCAAgC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAEtF,8FAA8F;IAC9F,OAAO,GAAG,KAAK,EAAE,OAAgB,EAAE,SAAiB,EAA0B,EAAE;QAC9E,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QAC3D,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,CAAC,kCAAkC,OAAO,IAAI,SAAS,EAAE,CAAC,CAAC;YACnE,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;YAC/C,OAAO,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC;QAC1B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,8EAA8E;YAC9E,4EAA4E;YAC5E,+EAA+E;YAC/E,+EAA+E;YAC/E,IAAI,CAAC,GAAG,CAAC,mCAAmC,OAAO,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC5G,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC,CAAC;CACH"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SlackLiveSender — the real Slack `SurfaceSender` for the live-test harness
|
|
3
|
+
* (docs/specs/live-user-channel-proof-standard.md §5.4). It posts a message into a
|
|
4
|
+
* Slack channel AS A NON-AGENT IDENTITY (a user/second-bot token, NOT Echo's own bot
|
|
5
|
+
* — Echo never replies to itself) and then waits for the AGENT's reply by polling the
|
|
6
|
+
* channel history for a message from the agent's bot user id.
|
|
7
|
+
*
|
|
8
|
+
* Parameterization is deliberate: the sender takes an already-constructed
|
|
9
|
+
* `SlackApiClient` (carrying the non-Echo sender token) + the agent's bot user id. So
|
|
10
|
+
* the CODE is complete and unit-testable here; the only thing that has to be provided
|
|
11
|
+
* at wiring time is the sender CREDENTIAL (a user token / second-bot token in the
|
|
12
|
+
* demo workspace). That credential is the one piece that may need provisioning — the
|
|
13
|
+
* logic does not.
|
|
14
|
+
*
|
|
15
|
+
* `awaitReply` identifies the agent's reply DETERMINISTICALLY (a message strictly
|
|
16
|
+
* after the sent ts whose author is the agent's bot user id), never a fuzzy guess, and
|
|
17
|
+
* resolves null on timeout (the harness records that as a FAIL with reason). The
|
|
18
|
+
* responder-MACHINE attribution (the cross-machine proof) is NOT done here — that is
|
|
19
|
+
* the RealChannelDriver's injected placement reader; this sender only returns the
|
|
20
|
+
* reply text + id.
|
|
21
|
+
*/
|
|
22
|
+
import type { SurfaceSender } from './RealChannelDriver.js';
|
|
23
|
+
import type { SendResult, ReplyResult } from './LiveTestHarness.js';
|
|
24
|
+
/** Minimal Slack client surface this sender needs (matches SlackApiClient.call). */
|
|
25
|
+
export interface SlackCaller {
|
|
26
|
+
call(method: string, params?: Record<string, unknown>): Promise<{
|
|
27
|
+
ok?: boolean;
|
|
28
|
+
ts?: string;
|
|
29
|
+
messages?: Array<{
|
|
30
|
+
ts: string;
|
|
31
|
+
user?: string;
|
|
32
|
+
bot_id?: string;
|
|
33
|
+
text?: string;
|
|
34
|
+
subtype?: string;
|
|
35
|
+
}>;
|
|
36
|
+
[k: string]: unknown;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
export interface SlackLiveSenderDeps {
|
|
40
|
+
/** A SlackApiClient constructed with the NON-AGENT sender token (the user-role identity). */
|
|
41
|
+
api: SlackCaller;
|
|
42
|
+
/** The agent's (Echo's) Slack bot user id — awaitReply waits for a reply authored by THIS id. */
|
|
43
|
+
agentBotUserId: string;
|
|
44
|
+
/** Poll cadence while awaiting a reply. Default 2000ms. */
|
|
45
|
+
pollIntervalMs?: number;
|
|
46
|
+
/** Injected for tests; defaults to real timers / clock. */
|
|
47
|
+
sleep?: (ms: number) => Promise<void>;
|
|
48
|
+
now?: () => number;
|
|
49
|
+
logger?: (m: string) => void;
|
|
50
|
+
}
|
|
51
|
+
export declare class SlackLiveSender implements SurfaceSender {
|
|
52
|
+
private readonly d;
|
|
53
|
+
constructor(deps: SlackLiveSenderDeps);
|
|
54
|
+
private now;
|
|
55
|
+
private sleep;
|
|
56
|
+
private log;
|
|
57
|
+
send(channelId: string, text: string): Promise<SendResult>;
|
|
58
|
+
awaitReply(channelId: string, opts: {
|
|
59
|
+
timeoutMs: number;
|
|
60
|
+
afterMessageId?: string;
|
|
61
|
+
}): Promise<Omit<ReplyResult, 'responderMachineId'> | null>;
|
|
62
|
+
/** Find the FIRST agent-authored message strictly after `after` (oldest-first). */
|
|
63
|
+
private findAgentReply;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=SlackLiveSender.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SlackLiveSender.d.ts","sourceRoot":"","sources":["../../src/core/SlackLiveSender.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEpE,oFAAoF;AACpF,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;QAC9D,EAAE,CAAC,EAAE,OAAO,CAAC;QACb,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,QAAQ,CAAC,EAAE,KAAK,CAAC;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAClG,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;KACtB,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,mBAAmB;IAClC,6FAA6F;IAC7F,GAAG,EAAE,WAAW,CAAC;IACjB,iGAAiG;IACjG,cAAc,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,qBAAa,eAAgB,YAAW,aAAa;IACnD,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAsB;gBAC5B,IAAI,EAAE,mBAAmB;IAErC,OAAO,CAAC,GAAG;YACG,KAAK;IAGnB,OAAO,CAAC,GAAG;IAEL,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAU1D,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,oBAAoB,CAAC,GAAG,IAAI,CAAC;IAelJ,mFAAmF;YACrE,cAAc;CAkB7B"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SlackLiveSender — the real Slack `SurfaceSender` for the live-test harness
|
|
3
|
+
* (docs/specs/live-user-channel-proof-standard.md §5.4). It posts a message into a
|
|
4
|
+
* Slack channel AS A NON-AGENT IDENTITY (a user/second-bot token, NOT Echo's own bot
|
|
5
|
+
* — Echo never replies to itself) and then waits for the AGENT's reply by polling the
|
|
6
|
+
* channel history for a message from the agent's bot user id.
|
|
7
|
+
*
|
|
8
|
+
* Parameterization is deliberate: the sender takes an already-constructed
|
|
9
|
+
* `SlackApiClient` (carrying the non-Echo sender token) + the agent's bot user id. So
|
|
10
|
+
* the CODE is complete and unit-testable here; the only thing that has to be provided
|
|
11
|
+
* at wiring time is the sender CREDENTIAL (a user token / second-bot token in the
|
|
12
|
+
* demo workspace). That credential is the one piece that may need provisioning — the
|
|
13
|
+
* logic does not.
|
|
14
|
+
*
|
|
15
|
+
* `awaitReply` identifies the agent's reply DETERMINISTICALLY (a message strictly
|
|
16
|
+
* after the sent ts whose author is the agent's bot user id), never a fuzzy guess, and
|
|
17
|
+
* resolves null on timeout (the harness records that as a FAIL with reason). The
|
|
18
|
+
* responder-MACHINE attribution (the cross-machine proof) is NOT done here — that is
|
|
19
|
+
* the RealChannelDriver's injected placement reader; this sender only returns the
|
|
20
|
+
* reply text + id.
|
|
21
|
+
*/
|
|
22
|
+
export class SlackLiveSender {
|
|
23
|
+
d;
|
|
24
|
+
constructor(deps) { this.d = deps; }
|
|
25
|
+
now() { return (this.d.now ?? Date.now)(); }
|
|
26
|
+
async sleep(ms) {
|
|
27
|
+
return (this.d.sleep ?? ((m) => new Promise((r) => setTimeout(r, m))))(ms);
|
|
28
|
+
}
|
|
29
|
+
log(m) { this.d.logger?.(`[slack-live-sender] ${m}`); }
|
|
30
|
+
async send(channelId, text) {
|
|
31
|
+
const res = await this.d.api.call('chat.postMessage', { channel: channelId, text });
|
|
32
|
+
if (!res.ts) {
|
|
33
|
+
// A post with no ts is a real failure — surface it (the harness records a driver
|
|
34
|
+
// error FAIL), never fabricate a messageId.
|
|
35
|
+
throw new Error(`chat.postMessage returned no ts (ok=${res.ok}) — message not posted`);
|
|
36
|
+
}
|
|
37
|
+
return { messageId: res.ts };
|
|
38
|
+
}
|
|
39
|
+
async awaitReply(channelId, opts) {
|
|
40
|
+
const deadline = this.now() + opts.timeoutMs;
|
|
41
|
+
const pollMs = this.d.pollIntervalMs ?? 2000;
|
|
42
|
+
const after = opts.afterMessageId; // a Slack ts string; lexicographic compare works for ts
|
|
43
|
+
// Poll at least once even if timeoutMs is ~0.
|
|
44
|
+
for (let first = true; first || this.now() < deadline; first = false) {
|
|
45
|
+
const reply = await this.findAgentReply(channelId, after);
|
|
46
|
+
if (reply)
|
|
47
|
+
return reply;
|
|
48
|
+
if (this.now() >= deadline)
|
|
49
|
+
break;
|
|
50
|
+
await this.sleep(pollMs);
|
|
51
|
+
}
|
|
52
|
+
this.log(`no agent reply in ${channelId} within ${opts.timeoutMs}ms`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/** Find the FIRST agent-authored message strictly after `after` (oldest-first). */
|
|
56
|
+
async findAgentReply(channelId, after) {
|
|
57
|
+
const res = await this.d.api.call('conversations.history', {
|
|
58
|
+
channel: channelId,
|
|
59
|
+
...(after ? { oldest: after, inclusive: false } : {}),
|
|
60
|
+
limit: 100,
|
|
61
|
+
});
|
|
62
|
+
const messages = res.messages ?? [];
|
|
63
|
+
// conversations.history returns newest-first; scan oldest-first so we return the
|
|
64
|
+
// EARLIEST agent reply after the prompt (deterministic).
|
|
65
|
+
const oldestFirst = [...messages].sort((a, b) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0));
|
|
66
|
+
for (const m of oldestFirst) {
|
|
67
|
+
if (after && !(m.ts > after))
|
|
68
|
+
continue; // strictly-after guard (belt + suspenders vs `oldest`)
|
|
69
|
+
if (m.user !== this.d.agentBotUserId)
|
|
70
|
+
continue; // only the AGENT's reply
|
|
71
|
+
const text = m.text ?? '';
|
|
72
|
+
return { text, messageId: m.ts };
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=SlackLiveSender.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SlackLiveSender.js","sourceRoot":"","sources":["../../src/core/SlackLiveSender.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA4BH,MAAM,OAAO,eAAe;IACT,CAAC,CAAsB;IACxC,YAAY,IAAyB,IAAI,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAEjD,GAAG,KAAa,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IACpD,KAAK,CAAC,KAAK,CAAC,EAAU;QAC5B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC3F,CAAC;IACO,GAAG,CAAC,CAAS,IAAU,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,uBAAuB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAE7E,KAAK,CAAC,IAAI,CAAC,SAAiB,EAAE,IAAY;QACxC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,iFAAiF;YACjF,4CAA4C;YAC5C,MAAM,IAAI,KAAK,CAAC,uCAAuC,GAAG,CAAC,EAAE,wBAAwB,CAAC,CAAC;QACzF,CAAC;QACD,OAAO,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,SAAiB,EAAE,IAAoD;QACtF,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC;QAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,cAAc,IAAI,IAAI,CAAC;QAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC,wDAAwD;QAC3F,8CAA8C;QAC9C,KAAK,IAAI,KAAK,GAAG,IAAI,EAAE,KAAK,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,KAAK,GAAG,KAAK,EAAE,CAAC;YACrE,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;YAC1D,IAAI,KAAK;gBAAE,OAAO,KAAK,CAAC;YACxB,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ;gBAAE,MAAM;YAClC,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC3B,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,qBAAqB,SAAS,WAAW,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC;QACtE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,mFAAmF;IAC3E,KAAK,CAAC,cAAc,CAAC,SAAiB,EAAE,KAAc;QAC5D,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,uBAAuB,EAAE;YACzD,OAAO,EAAE,SAAS;YAClB,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrD,KAAK,EAAE,GAAG;SACX,CAAC,CAAC;QACH,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC;QACpC,iFAAiF;QACjF,yDAAyD;QACzD,MAAM,WAAW,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3F,KAAK,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC;YAC5B,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC;gBAAE,SAAS,CAAC,uDAAuD;YAC/F,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,cAAc;gBAAE,SAAS,CAAC,yBAAyB;YACzE,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;YAC1B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;QACnC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-16T12:37:17.394Z",
|
|
5
|
+
"instarVersion": "1.3.598",
|
|
6
6
|
"entryCount": 201,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Two more pure building blocks for the live-test harness (spec live-user-channel-proof-standard §5.4/§5.6), ships DARK (not wired):
|
|
9
|
+
- `SlackLiveSender` — a real Slack `SurfaceSender`: posts as a non-agent identity via `chat.postMessage`, awaits the agent's reply by polling `conversations.history` (deterministic: a message strictly after the prompt, authored by the agent's bot user id). Parameterized on the sender token so only the credential needs provisioning.
|
|
10
|
+
- `PlacementResponderReader` — the injectable `resolveResponderMachine` for RealChannelDriver: maps (surface, channelId)→topic, reads `/pool/placement` `owner` (the machine that actually holds the seat — the cross-machine proof), tolerates read errors (returns null).
|
|
11
|
+
|
|
12
|
+
## What to Tell Your User
|
|
13
|
+
|
|
14
|
+
Nothing yet — internal infrastructure for the gold-standard live-testing harness, ships dark (no runtime surface, no behavior change). The payoff is when the harness drives the real Slack channel and proves which machine served the reply.
|
|
15
|
+
|
|
16
|
+
## Summary of New Capabilities
|
|
17
|
+
|
|
18
|
+
None user-facing. Internally: the real Slack drive + the cross-machine "which machine answered" reader that the live-test harness needs. No new routes, no config, no flags.
|
|
19
|
+
|
|
20
|
+
## Evidence
|
|
21
|
+
|
|
22
|
+
- `tests/unit/SlackLiveSender.test.ts` — 7 tests: post→ts, no-ts throw, agent-reply-after-prompt, ignores non-agent + stale messages, poll-until-appears, timeout→null, no-afterId.
|
|
23
|
+
- `tests/unit/PlacementResponderReader.test.ts` — 7 tests: telegram channel==topic, owner reflects seat move, slack channel→topic mapping, no-topic→null (no fetch), owner-null→null, fetch-error→null, arrow-bound detached reference.
|
|
24
|
+
- `tsc --noEmit` clean. instar-dev gate green.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Side-Effects Review — Live-test Slack sender + placement responder reader
|
|
2
|
+
|
|
3
|
+
**Slug:** live-test-slack-sender
|
|
4
|
+
**Spec:** docs/specs/live-user-channel-proof-standard.md §5.4 (Platform-Sanctioned Automation) + §5.6 (deterministic cross-machine proof)
|
|
5
|
+
**Files:** src/core/SlackLiveSender.ts, src/core/PlacementResponderReader.ts, tests/unit/SlackLiveSender.test.ts, tests/unit/PlacementResponderReader.test.ts
|
|
6
|
+
**Posture:** ships DARK — two pure/injectable building blocks, NOT wired into server.ts. No route, no runtime construction. Stacked on the driver core (PR #1195).
|
|
7
|
+
|
|
8
|
+
## What it is
|
|
9
|
+
- **SlackLiveSender** — a real Slack `SurfaceSender`: posts a message AS A NON-AGENT identity (a user/second-bot token, injected) via `chat.postMessage`, and `awaitReply` polls `conversations.history` for the AGENT's reply (a message strictly after the sent ts authored by the agent's bot user id). Deterministic match; null on timeout; throws (never fabricates an id) if a post returns no ts.
|
|
10
|
+
- **PlacementResponderReader** — the injectable `resolveResponderMachine` for RealChannelDriver: maps (surface, channelId)→topic, reads `GET /pool/placement` `owner`, returns the serving machine id. Tolerates read errors (returns null). Reads `owner` (who holds the seat), NOT `pinnedTo` (the intent) — the distinction IS the capstone.
|
|
11
|
+
|
|
12
|
+
## Phase 1 — Principle check (signal vs authority)
|
|
13
|
+
Neither module is a decision point. SlackLiveSender is transport (post + poll); PlacementResponderReader is a read that returns a machine id. Neither blocks, filters, or gates anything — they produce data the harness asserts on. No brittle blocking authority added. Compliant.
|
|
14
|
+
|
|
15
|
+
## Phase 4 — Side-effects answers
|
|
16
|
+
1. **Over-block** — n/a (no block surface). Worst case: SlackLiveSender's reply-match is too strict and misses a genuine agent reply → the scenario records a FAIL/no-reply. Mitigation: it matches on the agent's bot user id (the deterministic author), and polls the full window.
|
|
17
|
+
2. **Under-block** — n/a. A risk: matching a STALE agent message as the reply. Mitigated by the strictly-after-ts guard (both via `oldest`+`inclusive:false` AND an explicit `m.ts > after` belt-and-suspenders) and oldest-first scan returning the EARLIEST post-prompt agent message.
|
|
18
|
+
3. **Level-of-abstraction fit** — correct: both are the concrete adapters the harness's injected seams (`SurfaceSender`, `resolveResponderMachine`) were designed for. They wrap existing surfaces (`SlackApiClient`, `/pool/placement`) rather than reinventing them.
|
|
19
|
+
4. **Signal vs authority** — compliant (transport + read, no authority). See Phase 1.
|
|
20
|
+
5. **Interactions** — none yet (dark, unwired). SlackLiveSender uses a SEPARATE sender token from Echo's bot, so it can't be confused with Echo's own outbound; it reads history read-only. PlacementResponderReader only GETs placement. No shadowing/double-fire.
|
|
21
|
+
6. **External surfaces** — when wired, SlackLiveSender WILL post a real message into a Slack channel (the demo workspace) and PlacementResponderReader WILL GET the local placement route. In THIS increment: no wiring, so no external surface. The sender token/identity is the one piece that needs provisioning (a user/second-bot token in the demo workspace) — the code is parameterized on it.
|
|
22
|
+
7. **Multi-machine posture** — PlacementResponderReader IS the multi-machine attribution: it reads the authoritative `/pool/placement` (proxied to the lease-holder), so it answers "which machine served this" correctly regardless of which machine runs the harness. SlackLiveSender is machine-agnostic (talks to Slack's API). No single-machine assumption.
|
|
23
|
+
8. **Rollback cost** — trivial: dark, unwired. Revert the commit.
|
|
24
|
+
|
|
25
|
+
## No-deferrals
|
|
26
|
+
The Telegram live sender + the runner route are the NEXT tracked increment (CMT-1568, `.instar/plans/live-test-harness-drivers-BUILD.md`), not a deferral of this one. This increment is complete + fully unit-tested (14 tests). The Slack/Telegram SENDER-IDENTITY provisioning (a user token / demo bot) is an external-credential dependency tracked in the plan, explicitly NOT a code deferral — the code is parameterized so only the credential is missing.
|
|
27
|
+
|
|
28
|
+
## Phase 5 — Second-pass review
|
|
29
|
+
Not required: neither module adds a block/allow decision, a session-lifecycle action, a sentinel/guard/gate/watchdog, or a coherence/trust surface (the Phase-5 triggers). Both are transport/read adapters. The driver core that DOES touch the §5.3 block (DemoChannelRegistry) had its second-pass in PR #1195.
|