@xfey/tutti 0.1.103 → 0.1.105

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.
Files changed (106) hide show
  1. package/README.md +9 -4
  2. package/dist/chat-assistant/index.d.ts +6 -1
  3. package/dist/chat-assistant/index.js +22 -12
  4. package/dist/collaboration-ingestion/external-source.d.ts +15 -0
  5. package/dist/collaboration-ingestion/external-source.js +120 -0
  6. package/dist/collaboration-ingestion/index.d.ts +5 -0
  7. package/dist/collaboration-ingestion/index.js +4 -0
  8. package/dist/collaboration-ingestion/invalidations.d.ts +4 -0
  9. package/dist/collaboration-ingestion/invalidations.js +15 -0
  10. package/dist/collaboration-ingestion/main-chat.d.ts +33 -0
  11. package/dist/collaboration-ingestion/main-chat.js +67 -0
  12. package/dist/collaboration-ingestion/reference-files.d.ts +29 -0
  13. package/dist/collaboration-ingestion/reference-files.js +158 -0
  14. package/dist/collaboration-ingestion/types.d.ts +19 -0
  15. package/dist/collaboration-ingestion/types.js +2 -0
  16. package/dist/platform-outbound/coordinator.d.ts +17 -0
  17. package/dist/platform-outbound/coordinator.js +48 -0
  18. package/dist/platform-outbound/index.d.ts +5 -0
  19. package/dist/platform-outbound/index.js +5 -0
  20. package/dist/platform-outbound/repository.d.ts +30 -0
  21. package/dist/platform-outbound/repository.js +108 -0
  22. package/dist/platform-outbound/types.d.ts +38 -0
  23. package/dist/platform-outbound/types.js +2 -0
  24. package/dist/platform-outbound/worker.d.ts +36 -0
  25. package/dist/platform-outbound/worker.js +164 -0
  26. package/dist/server-shell/cli/cli.js +9 -0
  27. package/dist/server-shell/cli/errors.d.ts +1 -1
  28. package/dist/server-shell/cli/host-lifecycle.d.ts +2 -0
  29. package/dist/server-shell/cli/host-lifecycle.js +34 -1
  30. package/dist/server-shell/cli/host-relay-connection-manager.js +22 -11
  31. package/dist/server-shell/cli/host-relay-status.js +1 -0
  32. package/dist/server-shell/cli/host-runtime-endpoint.js +17 -1
  33. package/dist/server-shell/cli/host-server-runtime.d.ts +2 -0
  34. package/dist/server-shell/cli/host-server-runtime.js +88 -14
  35. package/dist/server-shell/cli/launch-command.d.ts +7 -0
  36. package/dist/server-shell/cli/launch-command.js +39 -1
  37. package/dist/server-shell/cli/launch.d.ts +2 -0
  38. package/dist/server-shell/cli/launch.js +3 -0
  39. package/dist/server-shell/cli/machine-local.d.ts +20 -0
  40. package/dist/server-shell/cli/machine-local.js +61 -1
  41. package/dist/server-shell/cli/managed-host.d.ts +16 -1
  42. package/dist/server-shell/cli/managed-host.js +191 -23
  43. package/dist/server-shell/cli/relay-registration.d.ts +4 -1
  44. package/dist/server-shell/cli/relay-registration.js +36 -7
  45. package/dist/server-shell/cli/runtime-commands.d.ts +5 -1
  46. package/dist/server-shell/cli/runtime-commands.js +9 -3
  47. package/dist/server-shell/http/host-tunnel.d.ts +5 -0
  48. package/dist/server-shell/http/host-tunnel.js +109 -0
  49. package/dist/server-shell/http/routes/local-control.d.ts +2 -0
  50. package/dist/server-shell/http/routes/local-control.js +2 -0
  51. package/dist/server-shell/http/routes/project-api/invalidations.d.ts +0 -1
  52. package/dist/server-shell/http/routes/project-api/invalidations.js +0 -3
  53. package/dist/server-shell/http/routes/project-api/messages-routes.js +32 -63
  54. package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +8 -4
  55. package/dist/server-shell/http/routes/project-api/openapi.d.ts +8 -4
  56. package/dist/server-shell/http/routes/project-api/reference-files-routes.js +25 -94
  57. package/dist/server-shell/http/routes/project-api/reference-files.d.ts +1 -11
  58. package/dist/server-shell/http/routes/project-api/reference-files.js +2 -87
  59. package/dist/server-shell/http/routes/project-api/schemas.d.ts +8 -4
  60. package/dist/server-shell/http/routes/project-api/schemas.js +8 -3
  61. package/dist/server-shell/http/routes/project-api/session-requirements.js +34 -2
  62. package/dist/server-shell/http/routes/project-api/types.d.ts +3 -1
  63. package/dist/server-shell/http/routes/project-api/workspace-projections.js +8 -2
  64. package/dist/server-shell/local-console/package-update-hosts.js +4 -1
  65. package/dist/server-shell/local-console/project-service.d.ts +13 -2
  66. package/dist/server-shell/local-console/project-service.js +73 -8
  67. package/dist/server-shell/local-console/server.js +10 -1
  68. package/dist/server-shell/local-console/session.d.ts +4 -0
  69. package/dist/server-shell/local-console/session.js +14 -0
  70. package/dist/server-shell/session/relay-session-context.d.ts +12 -1
  71. package/dist/server-shell/session/relay-session-context.js +19 -1
  72. package/migrations/0016_external_source_mappings.sql +141 -0
  73. package/migrations/0017_platform_outbound_intents.sql +31 -0
  74. package/migrations/README.md +3 -1
  75. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +26 -1
  76. package/node_modules/@tutti/relay-client/dist/host-control.js +62 -0
  77. package/node_modules/@tutti/relay-client/dist/tunnel-frames.js +32 -6
  78. package/node_modules/@tutti/relay-client/dist/tunnel-types.d.ts +5 -1
  79. package/node_modules/@tutti/shared/dist/domain/index.d.ts +9 -0
  80. package/node_modules/@tutti/shared/dist/domain/index.js +14 -0
  81. package/node_modules/@tutti/shared/dist/ids/index.d.ts +6 -0
  82. package/node_modules/@tutti/shared/dist/ids/index.js +4 -0
  83. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +4 -0
  84. package/node_modules/@tutti/shared/dist/schemas/api/index.js +2 -0
  85. package/node_modules/@tutti/shared/dist/schemas/api/local-console-account-handoffs.d.ts +13 -0
  86. package/node_modules/@tutti/shared/dist/schemas/api/local-console-account-handoffs.js +16 -0
  87. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.d.ts +43 -0
  88. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.js +44 -0
  89. package/node_modules/@tutti/shared/dist/schemas/domain/index.d.ts +7 -0
  90. package/node_modules/@tutti/shared/dist/schemas/domain/index.js +10 -0
  91. package/node_modules/@tutti/shared/dist/schemas/internal/index.d.ts +29 -0
  92. package/node_modules/@tutti/shared/dist/schemas/internal/index.js +24 -1
  93. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.d.ts +1346 -0
  94. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.js +516 -0
  95. package/node_modules/@tutti/shared/dist/utils/feishu-diagnostics/index.d.ts +10 -0
  96. package/node_modules/@tutti/shared/dist/utils/feishu-diagnostics/index.js +19 -0
  97. package/node_modules/@tutti/shared/dist/utils/index.d.ts +1 -0
  98. package/node_modules/@tutti/shared/dist/utils/index.js +1 -0
  99. package/node_modules/@tutti/shared/dist/utils/redaction/index.d.ts +1 -1
  100. package/node_modules/@tutti/shared/dist/utils/redaction/index.js +18 -0
  101. package/package.json +1 -1
  102. package/web/assets/{homepage-motion-scene-CPs4EYQq.js → homepage-motion-scene-BnRTgItL.js} +1 -1
  103. package/web/assets/{index-CxCTqGFO.css → index-CTMOjCgU.css} +1 -1
  104. package/web/assets/index-CmzEr4-V.js +69 -0
  105. package/web/index.html +2 -2
  106. package/web/assets/index-D3lwP_UR.js +0 -69
@@ -0,0 +1,48 @@
1
+ import { createPlatformOutboundIntent } from "./repository.js";
2
+ export class HostPlatformOutboundCoordinator {
3
+ worker;
4
+ logger;
5
+ now;
6
+ constructor(worker, logger, now = () => new Date()) {
7
+ this.worker = worker;
8
+ this.logger = logger;
9
+ this.now = now;
10
+ }
11
+ captureAgentResponse(tx, input) {
12
+ const result = createPlatformOutboundIntent(tx, {
13
+ triggerMessageId: input.triggerMessage.id,
14
+ canonicalMessageId: input.responseMessage.id,
15
+ now: this.now,
16
+ });
17
+ if (result === null) {
18
+ return undefined;
19
+ }
20
+ return () => {
21
+ try {
22
+ this.logger.info({
23
+ stage: "platform_outbound_capture",
24
+ disposition: result.created ? "created" : "duplicate",
25
+ outbound_intent_ref: result.intent.intent_ref,
26
+ canonical_message_ref: result.intent.canonical_message_id,
27
+ trigger_message_ref: result.intent.trigger_message_id,
28
+ installation_ref: result.intent.installation_ref,
29
+ binding_ref: result.intent.binding_ref,
30
+ }, result.created
31
+ ? "Platform outbound intent created"
32
+ : "Platform outbound intent already exists");
33
+ }
34
+ catch {
35
+ // The committed canonical response and durable intent remain authoritative even when the
36
+ // machine-local log cannot be appended. The polling worker still guarantees handoff.
37
+ }
38
+ this.worker.wake();
39
+ };
40
+ }
41
+ start() {
42
+ this.worker.start();
43
+ }
44
+ stop() {
45
+ return this.worker.stop();
46
+ }
47
+ }
48
+ //# sourceMappingURL=coordinator.js.map
@@ -0,0 +1,5 @@
1
+ export * from "./coordinator.js";
2
+ export * from "./repository.js";
3
+ export * from "./types.js";
4
+ export * from "./worker.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ export * from "./coordinator.js";
2
+ export * from "./repository.js";
3
+ export * from "./types.js";
4
+ export * from "./worker.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,30 @@
1
+ import type { SqliteDatabase } from "../store/index.js";
2
+ import type { HostPlatformOutboundIntentRecord, HostPlatformOutboundWorkItem } from "./types.js";
3
+ export declare function createPlatformOutboundIntent(tx: SqliteDatabase, input: {
4
+ triggerMessageId: string;
5
+ canonicalMessageId: string;
6
+ now?: () => Date;
7
+ }): {
8
+ created: boolean;
9
+ intent: HostPlatformOutboundIntentRecord;
10
+ } | null;
11
+ export declare function recoverInterruptedPlatformOutboundIntents(db: SqliteDatabase, now?: () => Date): number;
12
+ export declare function claimDuePlatformOutboundIntent(db: SqliteDatabase, now?: () => Date): HostPlatformOutboundWorkItem | null;
13
+ export declare function markPlatformOutboundHandedOff(db: SqliteDatabase, input: {
14
+ intentRef: string;
15
+ deliveryRef: string;
16
+ now?: () => Date;
17
+ }): void;
18
+ export declare function markPlatformOutboundRetry(db: SqliteDatabase, input: {
19
+ intentRef: string;
20
+ errorCode: string;
21
+ nextAttemptAt: string;
22
+ now?: () => Date;
23
+ }): void;
24
+ export declare function markPlatformOutboundTerminal(db: SqliteDatabase, input: {
25
+ intentRef: string;
26
+ errorCode: string;
27
+ now?: () => Date;
28
+ }): void;
29
+ export declare function readPlatformOutboundIntent(db: SqliteDatabase, intentRef: string): HostPlatformOutboundIntentRecord | null;
30
+ //# sourceMappingURL=repository.d.ts.map
@@ -0,0 +1,108 @@
1
+ import { createIdempotencyKey } from "@tutti/shared/ids";
2
+ export function createPlatformOutboundIntent(tx, input) {
3
+ const existing = tx
4
+ .prepare("SELECT * FROM platform_outbound_intents WHERE canonical_message_id = ?")
5
+ .get(input.canonicalMessageId);
6
+ if (existing !== undefined) {
7
+ return { created: false, intent: existing };
8
+ }
9
+ const destination = tx
10
+ .prepare(`
11
+ SELECT platform, installation_ref, binding_ref
12
+ FROM external_source_mappings
13
+ WHERE message_id = ? AND platform = 'feishu'
14
+ `)
15
+ .get(input.triggerMessageId);
16
+ if (destination === undefined) {
17
+ return null;
18
+ }
19
+ const timestamp = (input.now ?? (() => new Date()))().toISOString();
20
+ const intentRef = createIdempotencyKey();
21
+ tx.prepare(`
22
+ INSERT INTO platform_outbound_intents (
23
+ intent_ref, platform, installation_ref, binding_ref,
24
+ trigger_message_id, canonical_message_id, disposition, attempt_count,
25
+ next_attempt_at, created_at, updated_at
26
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
27
+ `).run(intentRef, destination.platform, destination.installation_ref, destination.binding_ref, input.triggerMessageId, input.canonicalMessageId, "pending", 0, timestamp, timestamp, timestamp);
28
+ return {
29
+ created: true,
30
+ intent: tx
31
+ .prepare("SELECT * FROM platform_outbound_intents WHERE intent_ref = ?")
32
+ .get(intentRef),
33
+ };
34
+ }
35
+ export function recoverInterruptedPlatformOutboundIntents(db, now = () => new Date()) {
36
+ const timestamp = now().toISOString();
37
+ return db
38
+ .prepare(`
39
+ UPDATE platform_outbound_intents
40
+ SET disposition = 'retry_wait', error_code = 'worker_interrupted',
41
+ next_attempt_at = ?, updated_at = ?
42
+ WHERE disposition = 'sending'
43
+ `)
44
+ .run(timestamp, timestamp).changes;
45
+ }
46
+ export function claimDuePlatformOutboundIntent(db, now = () => new Date()) {
47
+ return db.transaction(() => {
48
+ const timestamp = now().toISOString();
49
+ const row = db
50
+ .prepare(`
51
+ SELECT platform_outbound_intents.*, messages.body
52
+ FROM platform_outbound_intents
53
+ JOIN messages ON messages.id = platform_outbound_intents.canonical_message_id
54
+ WHERE platform_outbound_intents.disposition IN ('pending', 'retry_wait')
55
+ AND (
56
+ platform_outbound_intents.next_attempt_at IS NULL
57
+ OR platform_outbound_intents.next_attempt_at <= ?
58
+ )
59
+ ORDER BY platform_outbound_intents.created_at, platform_outbound_intents.intent_ref
60
+ LIMIT 1
61
+ `)
62
+ .get(timestamp);
63
+ if (row === undefined) {
64
+ return null;
65
+ }
66
+ db.prepare(`
67
+ UPDATE platform_outbound_intents
68
+ SET disposition = 'sending', attempt_count = attempt_count + 1,
69
+ last_attempt_at = ?, next_attempt_at = NULL, updated_at = ?
70
+ WHERE intent_ref = ?
71
+ `).run(timestamp, timestamp, row.intent_ref);
72
+ const intent = db
73
+ .prepare("SELECT * FROM platform_outbound_intents WHERE intent_ref = ?")
74
+ .get(row.intent_ref);
75
+ return { intent, body: row.body };
76
+ })();
77
+ }
78
+ export function markPlatformOutboundHandedOff(db, input) {
79
+ const timestamp = (input.now ?? (() => new Date()))().toISOString();
80
+ db.prepare(`
81
+ UPDATE platform_outbound_intents
82
+ SET disposition = 'handed_off', delivery_ref = ?, error_code = NULL,
83
+ handed_off_at = ?, updated_at = ?
84
+ WHERE intent_ref = ?
85
+ `).run(input.deliveryRef, timestamp, timestamp, input.intentRef);
86
+ }
87
+ export function markPlatformOutboundRetry(db, input) {
88
+ const timestamp = (input.now ?? (() => new Date()))().toISOString();
89
+ db.prepare(`
90
+ UPDATE platform_outbound_intents
91
+ SET disposition = 'retry_wait', error_code = ?, next_attempt_at = ?, updated_at = ?
92
+ WHERE intent_ref = ?
93
+ `).run(input.errorCode, input.nextAttemptAt, timestamp, input.intentRef);
94
+ }
95
+ export function markPlatformOutboundTerminal(db, input) {
96
+ const timestamp = (input.now ?? (() => new Date()))().toISOString();
97
+ db.prepare(`
98
+ UPDATE platform_outbound_intents
99
+ SET disposition = 'terminal', error_code = ?, next_attempt_at = NULL, updated_at = ?
100
+ WHERE intent_ref = ?
101
+ `).run(input.errorCode, timestamp, input.intentRef);
102
+ }
103
+ export function readPlatformOutboundIntent(db, intentRef) {
104
+ return (db
105
+ .prepare("SELECT * FROM platform_outbound_intents WHERE intent_ref = ?")
106
+ .get(intentRef) ?? null);
107
+ }
108
+ //# sourceMappingURL=repository.js.map
@@ -0,0 +1,38 @@
1
+ import type { IdempotencyKey, MessageId } from "@tutti/shared/ids";
2
+ import type { MessageProjection } from "@tutti/shared/schemas/api";
3
+ import type { PlatformOutboundHandoffResponse } from "@tutti/shared/schemas/internal";
4
+ export type HostPlatformOutboundIntentDisposition = "pending" | "sending" | "retry_wait" | "handed_off" | "terminal";
5
+ export type HostPlatformOutboundIntentRecord = {
6
+ intent_ref: IdempotencyKey;
7
+ platform: "feishu";
8
+ installation_ref: string;
9
+ binding_ref: string;
10
+ trigger_message_id: MessageId;
11
+ canonical_message_id: MessageId;
12
+ disposition: HostPlatformOutboundIntentDisposition;
13
+ attempt_count: number;
14
+ next_attempt_at: string | null;
15
+ last_attempt_at: string | null;
16
+ delivery_ref: string | null;
17
+ error_code: string | null;
18
+ created_at: string;
19
+ updated_at: string;
20
+ handed_off_at: string | null;
21
+ };
22
+ export type HostPlatformOutboundWorkItem = {
23
+ intent: HostPlatformOutboundIntentRecord;
24
+ body: string;
25
+ };
26
+ export type HostPlatformOutboundTransport = {
27
+ deliver(item: HostPlatformOutboundWorkItem): Promise<PlatformOutboundHandoffResponse>;
28
+ };
29
+ export type HostPlatformOutboundLogger = {
30
+ info(fields: Record<string, unknown>, message: string): void;
31
+ warn(fields: Record<string, unknown>, message: string): void;
32
+ error(fields: Record<string, unknown>, message: string): void;
33
+ };
34
+ export type HostPlatformOutboundAgentResponseInput = {
35
+ triggerMessage: MessageProjection;
36
+ responseMessage: MessageProjection;
37
+ };
38
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,36 @@
1
+ import type { HostProjectStore } from "../store/index.js";
2
+ import type { HostPlatformOutboundIntentRecord, HostPlatformOutboundLogger, HostPlatformOutboundTransport } from "./types.js";
3
+ export type HostPlatformOutboundRunResult = {
4
+ kind: "idle";
5
+ } | {
6
+ kind: "handed_off";
7
+ intent: HostPlatformOutboundIntentRecord;
8
+ } | {
9
+ kind: "retry_wait" | "terminal";
10
+ intent: HostPlatformOutboundIntentRecord;
11
+ error_code: string;
12
+ };
13
+ export declare class HostPlatformOutboundWorker {
14
+ private readonly store;
15
+ private readonly transport;
16
+ private readonly logger;
17
+ private readonly now;
18
+ private readonly pollIntervalMs;
19
+ private readonly maxAttempts;
20
+ private timer;
21
+ private current;
22
+ private stopped;
23
+ private wakeRequested;
24
+ constructor(store: HostProjectStore, transport: HostPlatformOutboundTransport, logger: HostPlatformOutboundLogger, options?: {
25
+ now?: () => Date;
26
+ pollIntervalMs?: number;
27
+ maxAttempts?: number;
28
+ });
29
+ start(): void;
30
+ wake(): void;
31
+ stop(): Promise<void>;
32
+ runOnce(): Promise<HostPlatformOutboundRunResult>;
33
+ private markFailure;
34
+ private schedule;
35
+ }
36
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1,164 @@
1
+ import { claimDuePlatformOutboundIntent, markPlatformOutboundHandedOff, markPlatformOutboundRetry, markPlatformOutboundTerminal, readPlatformOutboundIntent, recoverInterruptedPlatformOutboundIntents, } from "./repository.js";
2
+ const DEFAULT_POLL_INTERVAL_MS = 500;
3
+ const DEFAULT_MAX_ATTEMPTS = 8;
4
+ const MAX_BACKOFF_MS = 5 * 60 * 1_000;
5
+ function boundedPlatformText(value) {
6
+ const trimmed = value.trim();
7
+ const characters = [...trimmed];
8
+ return characters.length <= 4_000
9
+ ? trimmed
10
+ : `${characters.slice(0, 3_997).join("")}...`;
11
+ }
12
+ export class HostPlatformOutboundWorker {
13
+ store;
14
+ transport;
15
+ logger;
16
+ now;
17
+ pollIntervalMs;
18
+ maxAttempts;
19
+ timer;
20
+ current;
21
+ stopped = true;
22
+ wakeRequested = false;
23
+ constructor(store, transport, logger, options = {}) {
24
+ this.store = store;
25
+ this.transport = transport;
26
+ this.logger = logger;
27
+ this.now = options.now ?? (() => new Date());
28
+ this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
29
+ this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
30
+ }
31
+ start() {
32
+ if (!this.stopped) {
33
+ return;
34
+ }
35
+ this.stopped = false;
36
+ const recovered = recoverInterruptedPlatformOutboundIntents(this.store.db, this.now);
37
+ if (recovered > 0) {
38
+ this.logger.warn({ stage: "platform_outbound_recovery", disposition: "retry_wait", count: recovered }, "Interrupted platform outbound intents recovered");
39
+ }
40
+ this.schedule(0);
41
+ }
42
+ wake() {
43
+ if (this.stopped) {
44
+ return;
45
+ }
46
+ if (this.current !== undefined) {
47
+ this.wakeRequested = true;
48
+ return;
49
+ }
50
+ if (this.timer !== undefined) {
51
+ clearTimeout(this.timer);
52
+ }
53
+ this.schedule(0);
54
+ }
55
+ async stop() {
56
+ this.stopped = true;
57
+ if (this.timer !== undefined) {
58
+ clearTimeout(this.timer);
59
+ this.timer = undefined;
60
+ }
61
+ await this.current;
62
+ }
63
+ async runOnce() {
64
+ const item = claimDuePlatformOutboundIntent(this.store.db, this.now);
65
+ if (item === null) {
66
+ return { kind: "idle" };
67
+ }
68
+ const fields = {
69
+ outbound_intent_ref: item.intent.intent_ref,
70
+ canonical_message_ref: item.intent.canonical_message_id,
71
+ trigger_message_ref: item.intent.trigger_message_id,
72
+ installation_ref: item.intent.installation_ref,
73
+ binding_ref: item.intent.binding_ref,
74
+ attempt: item.intent.attempt_count,
75
+ };
76
+ this.logger.info({ ...fields, stage: "platform_outbound_attempt", disposition: "sending" }, "Platform outbound handoff attempted");
77
+ try {
78
+ const response = await this.transport.deliver({
79
+ ...item,
80
+ body: boundedPlatformText(item.body),
81
+ });
82
+ if (response.disposition.kind === "accepted" || response.disposition.kind === "duplicate") {
83
+ markPlatformOutboundHandedOff(this.store.db, {
84
+ intentRef: item.intent.intent_ref,
85
+ deliveryRef: response.disposition.delivery_ref,
86
+ now: this.now,
87
+ });
88
+ const updated = readPlatformOutboundIntent(this.store.db, item.intent.intent_ref);
89
+ this.logger.info({
90
+ ...fields,
91
+ stage: "platform_outbound_handoff",
92
+ disposition: response.disposition.kind,
93
+ delivery_ref: response.disposition.delivery_ref,
94
+ }, "Platform outbound intent handed off");
95
+ return { kind: "handed_off", intent: updated };
96
+ }
97
+ if (response.disposition.kind === "unavailable") {
98
+ return this.markFailure(item.intent, response.disposition.reason_code, true, fields);
99
+ }
100
+ return this.markFailure(item.intent, response.disposition.reason_code, false, fields);
101
+ }
102
+ catch (error) {
103
+ const code = typeof error === "object" &&
104
+ error !== null &&
105
+ "code" in error &&
106
+ typeof error.code === "string"
107
+ ? error.code.slice(0, 128)
108
+ : "platform_outbound_transport_failed";
109
+ const retryable = typeof error === "object" &&
110
+ error !== null &&
111
+ "retryable" in error &&
112
+ error.retryable === false
113
+ ? false
114
+ : true;
115
+ return this.markFailure(item.intent, code, retryable, fields);
116
+ }
117
+ }
118
+ markFailure(intent, errorCode, retryable, fields) {
119
+ const terminal = !retryable || intent.attempt_count >= this.maxAttempts;
120
+ if (terminal) {
121
+ markPlatformOutboundTerminal(this.store.db, {
122
+ intentRef: intent.intent_ref,
123
+ errorCode,
124
+ now: this.now,
125
+ });
126
+ }
127
+ else {
128
+ const delay = Math.min(1_000 * 2 ** Math.max(0, intent.attempt_count - 1), MAX_BACKOFF_MS);
129
+ markPlatformOutboundRetry(this.store.db, {
130
+ intentRef: intent.intent_ref,
131
+ errorCode,
132
+ nextAttemptAt: new Date(this.now().getTime() + delay).toISOString(),
133
+ now: this.now,
134
+ });
135
+ }
136
+ const updated = readPlatformOutboundIntent(this.store.db, intent.intent_ref);
137
+ const disposition = terminal ? "terminal" : "retry_wait";
138
+ this.logger[terminal ? "error" : "warn"]({
139
+ ...fields,
140
+ stage: "platform_outbound_handoff",
141
+ disposition,
142
+ error_code: errorCode,
143
+ }, terminal
144
+ ? "Platform outbound intent failed terminally"
145
+ : "Platform outbound intent will retry");
146
+ return { kind: disposition, intent: updated, error_code: errorCode };
147
+ }
148
+ schedule(delay) {
149
+ if (this.stopped) {
150
+ return;
151
+ }
152
+ this.timer = setTimeout(() => {
153
+ this.timer = undefined;
154
+ this.current = this.runOnce().finally(() => {
155
+ this.current = undefined;
156
+ const nextDelay = this.wakeRequested ? 0 : this.pollIntervalMs;
157
+ this.wakeRequested = false;
158
+ this.schedule(nextDelay);
159
+ });
160
+ }, delay);
161
+ this.timer.unref?.();
162
+ }
163
+ }
164
+ //# sourceMappingURL=worker.js.map
@@ -2,6 +2,7 @@
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { resolveTuttiHome } from "../../providers/openai/index.js";
4
4
  import { formatCliError, formatCliErrorReason, LaunchError } from "./errors.js";
5
+ import { recordInternalHostStartupFailure } from "./managed-host.js";
5
6
  import { parseCliArgs, shouldEnsureDesktopIntegrationAutomatically } from "./args.js";
6
7
  import { confirmTextFromTty, runBackgroundLaunchCommand, runForegroundLaunchCommand, runInternalHostRunCommand, } from "./launch-command.js";
7
8
  import { resolveExistingProjectContext } from "./project-resolver.js";
@@ -362,8 +363,10 @@ async function runShutdownCommand(yes) {
362
363
  "Tutti is ready to uninstall.",
363
364
  ].join("\n");
364
365
  }
366
+ let activeCommand;
365
367
  try {
366
368
  const command = parseCliArgs(process.argv.slice(2));
369
+ activeCommand = command;
367
370
  if (shouldEnsureDesktopIntegrationAutomatically(command)) {
368
371
  ensureDesktopIntegrationForConsole();
369
372
  }
@@ -451,6 +454,12 @@ try {
451
454
  }
452
455
  }
453
456
  catch (error) {
457
+ if (activeCommand?.kind === "internal-host-run") {
458
+ recordInternalHostStartupFailure({
459
+ tuttiHome: resolveTuttiHome(process.env.TUTTI_HOME, process.cwd()),
460
+ error,
461
+ });
462
+ }
454
463
  console.error(formatCliError(error));
455
464
  process.exitCode = 1;
456
465
  }
@@ -1,4 +1,4 @@
1
- export type LaunchErrorCode = "unsafe_project_root" | "unsupported_tutti_schema" | "project_identity_conflict" | "git_bootstrap_failed" | "sensitive_file_detected" | "relay_unavailable" | "relay_registration_failed" | "host_not_running" | "shutdown_confirmation_required" | "shutdown_rejected" | "archive_confirmation_required" | "archive_rejected" | "forget_confirmation_required" | "forget_rejected" | "project_target_ambiguous" | "project_target_not_found" | "provider_configuration_required" | "provider_setup_cancelled" | "provider_validation_failed" | "store_migration_failed" | "existing_server_unhealthy" | "takeover_rejected" | "takeover_failed" | "port_unavailable" | "desktop_integration_unavailable" | "desktop_protocol_invalid" | "desktop_open_failed";
1
+ export type LaunchErrorCode = "unsafe_project_root" | "unsupported_tutti_schema" | "project_identity_conflict" | "project_surface_conflict" | "project_surface_mismatch" | "git_bootstrap_failed" | "sensitive_file_detected" | "relay_unavailable" | "relay_registration_failed" | "host_not_running" | "shutdown_confirmation_required" | "shutdown_rejected" | "archive_confirmation_required" | "archive_rejected" | "forget_confirmation_required" | "forget_rejected" | "project_target_ambiguous" | "project_target_not_found" | "provider_configuration_required" | "provider_setup_cancelled" | "provider_validation_failed" | "store_migration_failed" | "existing_server_unhealthy" | "takeover_rejected" | "takeover_failed" | "port_unavailable" | "desktop_integration_unavailable" | "desktop_protocol_invalid" | "desktop_open_failed";
2
2
  export declare class LaunchError extends Error {
3
3
  readonly code: LaunchErrorCode;
4
4
  readonly next: string;
@@ -1,4 +1,5 @@
1
1
  import type { ProjectId } from "@tutti/shared/ids";
2
+ import { type ProjectSurfaceProfile } from "@tutti/shared/domain";
2
3
  import { TUTTI_MAINLINE_BRANCH } from "./git-bootstrap.js";
3
4
  import { type FetchLike, type HostRuntimeEndpointProbe } from "./host-runtime-endpoint.js";
4
5
  import { type HostServerFactory, type HostServerHandle } from "./host-server-runtime.js";
@@ -12,6 +13,7 @@ export type { HostServerHandle } from "./host-server-runtime.js";
12
13
  export type HostRuntimeStatus = "created" | "connected" | "recovered" | "taken_over";
13
14
  export type HostRuntimeSummary = {
14
15
  status: HostRuntimeStatus;
16
+ surface_profile: ProjectSurfaceProfile;
15
17
  workspace_root: string;
16
18
  project_id: ProjectId;
17
19
  branch: typeof TUTTI_MAINLINE_BRANCH;
@@ -1,4 +1,5 @@
1
1
  import { resolve } from "node:path";
2
+ import { createProjectSurfaceProfile } from "@tutti/shared/domain";
2
3
  import { connectRelayHostTunnel, RelayHostControlClientError, refreshRelayJoinToken as refreshRelayJoinTokenWithRelay, registerRelayHostConnection as registerRelayHostConnectionWithRelay, } from "@tutti/relay-client";
3
4
  import { createHostServer } from "../http/create-server.js";
4
5
  import { createHostProjectApiTunnelRequestHandler, createHostProjectApiTunnelStreamRequestHandler, } from "../http/host-tunnel.js";
@@ -56,6 +57,7 @@ async function requireTakeoverConfirmation(options) {
56
57
  function createSummary(options) {
57
58
  const summary = {
58
59
  status: options.status,
60
+ surface_profile: options.surfaceProfile,
59
61
  workspace_root: options.workspaceRoot,
60
62
  project_id: options.projectId,
61
63
  branch: TUTTI_MAINLINE_BRANCH,
@@ -129,6 +131,12 @@ export async function startLaunchProject(options) {
129
131
  let allowWorkspaceRootTakeover = false;
130
132
  if (context.existing_identity.kind === "present") {
131
133
  const projectId = context.existing_identity.project_id;
134
+ const existingBinding = readMachineProjectBinding(context.tutti_home, projectId);
135
+ const existingSurfaceProfile = existingBinding?.surface_profile ?? createProjectSurfaceProfile();
136
+ if (options.projectSurface !== undefined &&
137
+ existingSurfaceProfile.kind !== options.projectSurface) {
138
+ throw new LaunchError("project_surface_conflict", `This project is already configured for the ${existingSurfaceProfile.kind} collaboration surface`, "Use the existing project surface. Surface migration requires a separate explicit migration flow.");
139
+ }
132
140
  await reconcileReplacedWorkspaceHosts({
133
141
  tuttiHome: context.tutti_home,
134
142
  workspaceRoot: context.workspace_root,
@@ -181,6 +189,7 @@ export async function startLaunchProject(options) {
181
189
  kind: "connected",
182
190
  summary: createSummary({
183
191
  status: "connected",
192
+ surfaceProfile: existingSurfaceProfile,
184
193
  workspaceRoot: context.workspace_root,
185
194
  projectId,
186
195
  relayUrl: context.relay_url,
@@ -260,6 +269,9 @@ export async function startLaunchProject(options) {
260
269
  };
261
270
  const getLaunchStatus = () => hostLocalLaunchStatusFromConnectionManagerState(preparation.project_id, readConnectionState());
262
271
  const refreshRelayJoinUrl = async () => {
272
+ if (preparation.machine_local.binding.surface_profile.kind !== "standalone") {
273
+ return relayStateFromConnectionManagerState(preparation.relay_url, readConnectionState());
274
+ }
263
275
  if (relayRuntime.connectionManager === undefined) {
264
276
  return relayStateFromConnectionManagerState(preparation.relay_url, readConnectionState());
265
277
  }
@@ -296,10 +308,13 @@ export async function startLaunchProject(options) {
296
308
  heartbeatIntervalMs: registered.heartbeatIntervalMs,
297
309
  handleRequest: createHostProjectApiTunnelRequestHandler({
298
310
  app: host.app,
311
+ projectSurface: preparation.machine_local.binding.surface_profile.kind,
299
312
  trustedMetadataStore: host.trusted_relay_metadata_store,
313
+ integrationDelivery: host.integration_delivery,
300
314
  }),
301
315
  handleStreamRequest: createHostProjectApiTunnelStreamRequestHandler({
302
316
  baseUrl: host.runtime_endpoint.base_url,
317
+ projectSurface: preparation.machine_local.binding.surface_profile.kind,
303
318
  trustedMetadataStore: host.trusted_relay_metadata_store,
304
319
  }),
305
320
  }),
@@ -354,6 +369,7 @@ export async function startLaunchProject(options) {
354
369
  host,
355
370
  summary: createSummary({
356
371
  status: startupStatus,
372
+ surfaceProfile: preparation.machine_local.binding.surface_profile,
357
373
  workspaceRoot: preparation.workspace_root,
358
374
  projectId: preparation.project_id,
359
375
  relayUrl: preparation.relay_url,
@@ -374,7 +390,24 @@ export function formatLaunchLifecycleResult(result, options = {}) {
374
390
  if (summary.setup_url !== undefined) {
375
391
  fields.push(urlLine("Setup URL", summary.setup_url, options));
376
392
  }
377
- if (summary.relay?.join_url !== undefined) {
393
+ if (summary.surface_profile.kind === "feishu") {
394
+ fields.push("Collaboration surface: Feishu");
395
+ if (summary.relay?.feishu_setup?.state === "pairing_required" &&
396
+ summary.relay.feishu_setup.visibility === "visible_once") {
397
+ fields.push(`Connect command: ${summary.relay.feishu_setup.command}`);
398
+ fields.push(`Connect code expires at: ${summary.relay.feishu_setup.expires_at}`);
399
+ }
400
+ else if (summary.relay?.feishu_setup?.state === "pairing_required") {
401
+ fields.push("Feishu conversation: pairing code is not recoverable; restart from Local Console to rotate it");
402
+ }
403
+ else if (summary.relay?.feishu_setup?.state === "connected") {
404
+ fields.push("Feishu conversation: connected");
405
+ }
406
+ else {
407
+ fields.push("Feishu Owner: open this project through Local Console to connect an account");
408
+ }
409
+ }
410
+ else if (summary.relay?.join_url !== undefined) {
378
411
  fields.push(urlLine("Join URL", summary.relay.join_url, options));
379
412
  fields.push(formatJoinLinkValidity({
380
413
  ...(summary.relay.join_token_expires_at === undefined
@@ -12,17 +12,28 @@ class WorkspaceUnavailableError extends Error {
12
12
  const DEFAULT_RETRY_DELAYS_MS = [0, 1_000, 2_000, 5_000, 10_000, 30_000];
13
13
  const DEFAULT_JITTER_RATIO = 0.2;
14
14
  const DEFAULT_WORKSPACE_CHECK_INTERVAL_MS = 30_000;
15
- function mergeKnownJoinUrl(previous, next) {
16
- if (next.join_url !== undefined ||
17
- previous?.join_url === undefined ||
18
- previous.join_token_expires_at !== next.join_token_expires_at) {
19
- return next;
15
+ function mergeKnownVisibleCapabilities(previous, next) {
16
+ let merged = next;
17
+ if (next.join_url === undefined &&
18
+ previous?.join_url !== undefined &&
19
+ previous.join_token_expires_at === next.join_token_expires_at) {
20
+ merged = {
21
+ ...merged,
22
+ join_url: previous.join_url,
23
+ join_url_visibility: previous.join_url_visibility,
24
+ };
20
25
  }
21
- return {
22
- ...next,
23
- join_url: previous.join_url,
24
- join_url_visibility: previous.join_url_visibility,
25
- };
26
+ const previousSetup = previous?.feishu_setup;
27
+ const nextSetup = next.feishu_setup;
28
+ if (previousSetup?.state === "pairing_required" &&
29
+ previousSetup.visibility === "visible_once" &&
30
+ nextSetup?.state === "pairing_required" &&
31
+ nextSetup.visibility === "not_recoverable" &&
32
+ nextSetup.expires_at !== undefined &&
33
+ previousSetup.expires_at === nextSetup.expires_at) {
34
+ merged = { ...merged, feishu_setup: previousSetup };
35
+ }
36
+ return merged;
26
37
  }
27
38
  function retryableConnectionError(error) {
28
39
  if (error instanceof RelayHostControlClientError) {
@@ -249,7 +260,7 @@ class DefaultHostRelayConnectionManager {
249
260
  tunnel.close(1000, "host stopping");
250
261
  throw new Error("Relay connection attempt was cancelled");
251
262
  }
252
- const connection = mergeKnownJoinUrl(this.lastConnection, registered.summary);
263
+ const connection = mergeKnownVisibleCapabilities(this.lastConnection, registered.summary);
253
264
  const previousTunnel = this.activeTunnel;
254
265
  this.pendingTunnel = undefined;
255
266
  this.activeTunnel = tunnel;
@@ -97,6 +97,7 @@ export function relayStatusFromLocalStatus(status) {
97
97
  join_token_reusable: status.relay.join_token_reusable ?? true,
98
98
  tunnel_url: status.relay.tunnel_url,
99
99
  connected_at: status.relay.connected_at,
100
+ ...(status.relay.feishu_setup === undefined ? {} : { feishu_setup: status.relay.feishu_setup }),
100
101
  };
101
102
  }
102
103
  //# sourceMappingURL=host-relay-status.js.map